Search code examples
androidandroid-mediasessionandroid-media3mediasession

MediaSession unable to play audio in android 10 and above


I am using MediaSessionService as a foreground service to play the audio (in my audio player android project). Previously I was using the ExoPlayer and a Service, that time I was fetching (getAudios(Activity activity) method) the audio files from device using ContentResolver and setting the audio data in the AudioModel object. And that time all are OK and worked good in Android 10. But when I switched to MediaSessionService and does all the instructions guided here the app works fine in Android 8 but the audios are not playing in Android 10. When I check the log I have seen that after buffering the player going to idle state which indicates that some error is happening with mediaItem (as I know so far). But the same works in Android 8 i.e. the player going to ready state after buffering state. I am wondering is there any other permissions requirement in Android 10? Let me highlight one point again that without using MediaSession, MediaController etc. all works fine in both Android 8 and 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 is the method which is used to set mediaItems to player. It extracts the id and uri from the model class and creates a mediaItem and then sets to the 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);
    }

Here I am requesting the permissions

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;
    }

}

Solution

  • You are getting error likely because of the introduction of Storage Access Framework (SAF) in Android versions 10 and above, the file in the download folder cannot be accessed with code. This helps to make user device more secure.

    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);
        }
    }
    

    Update 1

    This issue has been already there on exo player portal

    1. Source Error

    2. Issue Accessing local files

    3. EACCESS (Permission denied)

    4. Storage Access

    5. Support for File Descriptor

    6. Android SAF

    After that they have Updated on storage Access - https://github.com/google/ExoPlayer/commit/804dfe228ef95b99cdc0260eb4af80597db047da

    You can try retrieving a complete list of audio files using MediaScannerConnection after API Level 29.

    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 Developer Documentation:

    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