我使用 MediaSessionService 作为前台服务来播放音频(在我的音频播放器 android 项目中)。之前我使用 ExoPlayer 和 Service,当时我使用 ContentResolver 从设备获取(getAudios(Activity Activity) 方法)音频文件,并在 AudioModel 对象中设置音频数据。那时一切都很好,并且在 Android 10 中运行良好。但是当我切换到 MediaSessionService 并按照here指导的所有说明进行操作时,该应用程序在 Android 8 中运行良好,但音频在 Android 10 中无法播放。当我检查日志时我已经看到,缓冲后播放器进入空闲状态,这表明 mediaItem 发生了一些错误(据我所知)。但在 Android 8 中也是如此,即播放器在缓冲状态后进入就绪状态。我想知道Android 10还有其他权限要求吗? 让我再次强调一点,在不使用 MediaSession、MediaController 等的情况下,在 Android 8 和 10 中都可以正常工作。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
//..............>
<service
android:name=".service.AudioPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback"
android:permission="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<activity
//............
</activity>
</application>
</manifest>
public MutableLiveData<List<AudioModel>> getAudios(Activity activity) {
MutableLiveData<List<AudioModel>> mutableLiveData = new MutableLiveData<>();
List<AudioModel> audioModels = new ArrayList<>();
if (activity == null) {
mutableLiveData.setValue(null);
return mutableLiveData;
}
ContentResolver resolver = activity.getContentResolver();
Uri collections;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collections = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else collections = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = new String[]{
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DATE_ADDED,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.SIZE,
MediaStore.Audio.Media.DATA
};
try (Cursor cursor = resolver.query(collections, projection, null, null, null)) {
assert cursor != null;
int idCol = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
int nameCol = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME);
int albumCol = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
int artistCol = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
int dateAddedCol = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED);
int durationCol = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
int sizeCol = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE);
int dataCol = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
while (cursor.moveToNext()) {
AudioModel model = new AudioModel(Uri.parse(cursor.getString(dataCol)), cursor.getString(nameCol));
model.setMedia_id(cursor.getString(idCol));
model.setAlbum(cursor.getString(albumCol));
model.setArtist(cursor.getString(artistCol));
model.setDateAdded(cursor.getString(dateAddedCol));
model.setSize(cursor.getLong(sizeCol));
model.setDuration(cursor.getLong(durationCol));
audioModels.add(model);
}
} catch (NullPointerException e) {
Constants.LOG.log("Exception: " + e.getMessage());
}
mutableLiveData.setValue(audioModels);
return mutableLiveData;
}
@UnstableApi public class MediaSessionCallback implements MediaSession.Callback {
@NonNull
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(@NonNull MediaSession mediaSession, @NonNull MediaSession.ControllerInfo controller, @NonNull List<MediaItem> mediaItems) {
Constants.LOG.mediaSessionLog("MediaSession > onAddMediaItems, size -> "+mediaItems.size());
List<MediaItem> updatedMediaItems = mediaItems.stream().peek(
mediaItem ->
mediaItem.buildUpon()
.setUri(mediaItem.requestMetadata.mediaUri)
.build()).collect(Collectors.toList());
return Futures.immediateFuture(updatedMediaItems);
}
@NonNull
@OptIn(markerClass = UnstableApi.class) @Override
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onSetMediaItems(@NonNull MediaSession mediaSession, @NonNull MediaSession.ControllerInfo controller, @NonNull List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
Constants.LOG.mediaSessionLog("MediaSession > onAddMediaItems, size > "+mediaItems.size()+", startIndex > "+startIndex);
return MediaSession.Callback.super.onSetMediaItems(mediaSession, controller, mediaItems, startIndex, startPositionMs);
}
}
public class AudioPlaybackService extends MediaSessionService {
private ExoPlayer player;
private MediaSession mediaSession = null;
// Create your Player and MediaSession in the onCreate lifecycle event
@OptIn(markerClass = UnstableApi.class) @Override
public void onCreate() {
super.onCreate();
if (this.player == null) this.player = new ExoPlayer.Builder(this).build();
this.mediaSession = new MediaSession.Builder(this, player)
.setCallback(new MediaSessionCallback())
.build();
}
// The user dismissed the app from the recent tasks
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
Player player = this.mediaSession.getPlayer();
if (!player.getPlayWhenReady()
|| player.getMediaItemCount() == 0
|| player.getPlaybackState() == Player.STATE_ENDED) {
// Stop the service if not playing, continue playing in the background
// otherwise.
stopSelf();
}
}
@Nullable
@Override
public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
return this.mediaSession;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
if (intent == null || intent.getAction() == null) return Service.START_NOT_STICKY;
String action = intent.getAction();
this.player = PlayerCreator.getPlayer(getApplicationContext());
switch (action) {
case Constants.Service.START_AUDIO_PLAYBACK_FOREGROUND:
startForeground(Constants.Notification.AUDIO_PLAYING_NOTIFICATION_CHANNEL_ID, createNotification());
playMusic();
break;
case Constants.Service.STOP_AUDIO_PLAYBACK_FOREGROUND:
stopForeground(true);
playPause();
stopSelf();
break;
case Constants.Service.PLAY_PAUSE_AUDIO_PLAYBACK_FOREGROUND:
startForeground(Constants.Notification.AUDIO_PLAYING_NOTIFICATION_CHANNEL_ID, createNotification());
playPause();
break;
case Constants.Service.NEXT_AUDIO_PLAYBACK_FOREGROUND:
startForeground(Constants.Notification.AUDIO_PLAYING_NOTIFICATION_CHANNEL_ID, createNotification());
next();
break;
case Constants.Service.PREVIOUS_AUDIO_PLAYBACK_FOREGROUND:
startForeground(Constants.Notification.AUDIO_PLAYING_NOTIFICATION_CHANNEL_ID, createNotification());
previous();
break;
case Constants.Service.MEDIA_ITEM_TRANSITION:
startForeground(Constants.Notification.AUDIO_PLAYING_NOTIFICATION_CHANNEL_ID, createNotification());
break;
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
this.mediaSession.getPlayer().release();
this.mediaSession.release();
this.mediaSession = null;
super.onDestroy();
}
}
setUpPlayer 是用于将 mediaItems 设置为播放器的方法。它从模型类中提取 id 和 uri 并创建一个 mediaItem,然后设置为 MediaController
@OptIn(markerClass = UnstableApi.class)
private void setUpPlayer(List<AudioModel> audioModels) {
Constants.LOG.log("In setup player");
if (this.binding == null) this.onDestroy(); // TODO: Destroying the fragment
// Initialize Exoplayer;
this.mediaController.addListener(this);
// Setting the ExoPlayer MediaItems / MediaSources
List<MediaItem> mediaItemList = new ArrayList<>();
for (AudioModel file : audioModels) {
Constants.LOG.mediaSessionLog("AudioModel URI : "+file.getData());
MediaItem item = new MediaItem.Builder().setMediaId(file.getMedia_id()).setUri(file.getData()).build();
Constants.LOG.mediaSessionLog("Audio URI : "+item.requestMetadata.mediaUri);
mediaItemList.add(item);
}
this.mediaController.setMediaItems(mediaItemList);
this.mediaController.prepare();
this.binding.progressBarAudioFileLoad.setVisibility(View.GONE);
}
我在这里请求权限
public class HomeActivity extends AppCompatActivity {
private String[] permissions;
/**
* settingOpenResultLauncher is used to open setting for permission grant and check the result
*/
private ActivityResultLauncher<Intent> settingOpenResultLauncher;
private ActivityHomeBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
Constants.LOG.lifeCycleLog(this.getClass().getSimpleName()+this.INSTANCE_ID+": onCreate");
// Handle the splash screen transition.
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
//Do the below before initializing binding
this.permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
if(!isPermissionsGranted()) requestPermission(permissions);
this.settingOpenResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if(!isPermissionsGranted()) requestPermission(HomeActivity.this.permissions);
else finish();
});
}
private void requestPermission(String... permissions) {
// TODO: add other permissions which are needed
ActivityCompat.requestPermissions(HomeActivity.this,
permissions,
Constants.PERMISSIONS.PERMISSIONS_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == Constants.PERMISSIONS.PERMISSIONS_CODE) {
int i = 0;
for (; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
if(!shouldShowRequestPermissionRationale(permissions[i])) {
// This executes when second time requesting the permission
// TODO: Show any alert that why the permission(s) should be granted
openSetting();
} else requestPermission(permissions[i]);
}
}
}
}
/**
* This method is used to open settings when user selects don't show permission asking again
*/
private void openSetting() {
Intent i = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
i.setData(uri);
if (this.settingOpenResultLauncher == null) {
Toast.makeText(this, "Unable to open setting", Toast.LENGTH_SHORT).show();
return;
}
this.settingOpenResultLauncher.launch(i);
}
private boolean isPermissionsGranted() {
String[] permissions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions = new String[]{Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO};
} else {
permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
}
// TODO: for android 13+ which permissions are not handled, handle those below
for (String permission : permissions)
if (ContextCompat.checkSelfPermission(HomeActivity.this, permission)
== PackageManager.PERMISSION_DENIED)
return false;
return true;
}
}
您遇到的错误可能是由于 Android 版本 10 及更高版本中引入了存储访问框架 (SAF),无法使用代码访问下载文件夹中的文件。这有助于使用户设备更加安全。
private void loadAudioFromDownloadFolder() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("audio/*");
intent.setData(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
startActivityForResult(intent, REQUEST_CODE_FILE_PICKER);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_FILE_PICKER && resultCode == RESULT_OK) {
Uri selectedFileUri = data.getData();
MediaItem mediaItem = new MediaItem.Builder()
.setUri(selectedFileUri)
.build();
player.addMediaItem(mediaItem);
}
}
Android 开发者文档:
https://developer.android.com/guide/topics/providers/document-provider
https://developer.android.com/training/data-storage/shared/documents-files
中文章:
https://developer.android.com/training/data-storage/shared/media