我正在使用MediaSessionService作为前台服务来播放音频(在我的音频播放器Android项目中).之前我使用ExoPlayer和Service,当时我使用ContentResolver从设备获取(getAudios(活动活动)方法)音频文件并在AudioModel对象中设置音频数据.当时一切都很好,并且在Android 10中运行良好.但当我切换到MediaSessionService并执行here指导的所有指令时,该应用程序在Android 8中运行良好,但在Android 10中无法播放音频.当我判断日志(log)时,我看到缓冲后播放器进入空闲状态,这表明mediaProject发生了一些错误(据我所知).但在Android 8中也是如此,即播放器在缓冲状态后进入准备状态.我想知道Android 10中是否还有其他权限要求? 让我再次强调一点,即在不使用MediaPhone、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是用于将媒体项目设置为播放器的方法.它从模型类中提取id和uri并创建一个mediaProject,然后设置为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);
    }
}

更新 1

exo玩家门户上已经存在了这个问题

  1. Source Error

  2. Issue Accessing local files

  3. EACCESS (Permission denied)

  4. Storage Access

  5. Support for File Descriptor

  6. Android SAF

之后,他们更新了存储Access - https://github.com/google/ExoPlayer/commit/804dfe228ef95b99cdc0260eb4af80597db047da

您可以try 在API级别29之后使用MediaScannerConnection检索完整的音频文件列表.

public class MyMediaScannerClient implements MediaScannerConnection.MediaScannerConnectionClient {

    private Context context;
    private String filePath;
    private List<AudioModel> mediaItemList = new ArrayList<>();

    public MyMediaScannerClient(Context context, String filePath) {
        this.context = context;
        this.filePath = filePath;
    }

    @Override
    public void onScanCompleted(String path, Uri uri) {
        Toast.makeText(context, "Audio Scan completed!", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onMediaScannerConnected() {
        MediaScannerConnection connection = new MediaScannerConnection(context, this);
        connection.connect();
        getAudioFilesFromDirectory();
    }

    private void getAudioFilesFromDirectory() {
        File directory = new File(filePath);
        if (directory.exists() && directory.isDirectory()) {
            File[] files = directory.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isFile() && isAudioFile(file)) {
                        String title = file.getName();
                        String data = file.getAbsolutePath();
                        AudioModel model = new AudioModel(Uri.parse(data), title);
                        mediaItemList.add(model);
                    }
                }
            }
        }
    }

    private boolean isAudioFile(File file) {
        String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(file.toString()));
        return mimeType != null && mimeType.startsWith("audio/");
    }

    public List<AudioModel> getMediaItemList() {
        return mediaItemList;
    }
}



String downloadDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
MyMediaScannerClient scannerClient = new MyMediaScannerClient(context, downloadDir);
scannerClient.scanFile();

List<AudioModel> audioModels = scannerClient.getMediaItemList();

// Prepare ExoPlayer and add MediaItems
for (AudioModel model : audioModels) {
    MediaSource mediaSource = DefaultDataSourceFactory(context)
        .createDataSource(Uri.parse(model.getData()));
    MediaItem mediaItem = new MediaItem.Builder()
        .setMediaSource(mediaSource)
        .build();
    player.addMediaItem(mediaItem);
}

Android开发人员文档:

https://developer.android.com/training/data-storage#scoped-storage

https://developer.android.com/guide/topics/providers/document-provider

https://developer.android.com/training/data-storage/shared/documents-files

Android相关问答推荐

使用不同的Google帐户登录

Android Bundle getSerializable(String?):'可序列化?&# 39、被抛弃了在Java中被弃用

如何在点击按钮时将字符串插入到文本字段中的光标位置?

在Android应用程序上使用Room数据库时,是否可以预先填充数据

ENV变量在gradle进程中没有更新

list 合并失败,AGP 8.3.0

我到底应该如何分享我的应用程序中的图片?

从未设置实时数据值

触发PurchasesUpdatedListener回调时,billingClient.launchBillingFlow之前设置的成员变量丢失

由于Xcode运行脚本阶段没有指定输出,在IOS Emulator中的KMM项目中生成失败

当提供非状态对象时,compose 如何进行重组

可组合函数无限地从视图模型获取值

任务:app:kaptGenerateStubsDebugKotlin执行失败. > 'compileDebugJavaWithJavac' 任务(当前目标是 1.8)

通过 setIntentScanningStrategyEnabled(true) 未检测到信标的 Android Beacon 库后台扫描

java.lang.ExceptionInInitializerError -- 原因:java.lang.NullPointerException

在Android RoomDB中使用Kotlin Flow和删除数据时如何解决错误?

在 Jetpack Compose 中清除列表时可组合不重组

如何在 compose 中使用 BottomSheetScaffold 为底页设置半展开高度?

在 Kotlin 客户端应用程序中发送 FCM 推送通知 - Firebase 云消息传递

react-native-config 在发布版本中不起作用