doublesymmetry / react-native-track-player

A fully fledged audio module created for music apps. Provides audio playback, external media controls, background mode and more!
https://rntp.dev/
Apache License 2.0
3.27k stars 1.01k forks source link

Support for Android Auto is not working #640

Closed dhaval-devstree closed 5 years ago

dhaval-devstree commented 5 years ago

Configuration

What react-native-track-player version are you using? -> dev-branch v1.1.5

Issue

--> Platform - Android --> Issue description : Android auto support is not working. Media metadata is not getting while synchronise application with android auto device.

Code

1. AnrdoidManifest.xml

**2. automotive_app_desc.xml** ``` ``` **3. MusicService.java** ``` public class MusicService extends HeadlessJsTaskService/*, MediaBrowserServiceCompat*/ { MusicManager manager; Handler handler; @Nullable @Override protected HeadlessJsTaskConfig getTaskConfig(Intent intent) { return new HeadlessJsTaskConfig("TrackPlayer", Arguments.createMap(), 0, true); } @Override public void onHeadlessJsTaskFinish(int taskId) { // Overridden to prevent the service from being terminated } public void emit(String event, Bundle data) { Intent intent = new Intent(Utils.EVENT_INTENT); intent.putExtra("event", event); if (data != null) intent.putExtra("data", data); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } public void destroy() { if (handler != null) { handler.removeMessages(0); handler = null; } if (manager != null) { manager.destroy(); manager = null; } } private void onStartForeground() { boolean serviceForeground = false; if (manager != null) { // The session is only active when the service is on foreground serviceForeground = manager.getMetadata().getSession().isActive(); } if (!serviceForeground) { ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager(); ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); // // Checks whether there is a React activity if (reactContext == null || !reactContext.hasCurrentActivity()) { String channel = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { channel = NotificationChannel.DEFAULT_CHANNEL_ID; } // Sets the service to foreground with an empty notification startForeground(1, new NotificationCompat.Builder(this, channel).build()); // Stops the service right after stopSelf(); } } } @Nullable @Override public IBinder onBind(Intent intent) { if (Utils.CONNECT_INTENT.equals(intent.getAction())) { return new MusicBinder(this, manager); } return super.onBind(intent); } /* Method for MediaBrowserServiceCompat @androidx.annotation.Nullable @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @androidx.annotation.Nullable Bundle rootHints) { return new BrowserRoot("root", rootHints); } @Override public void onLoadChildren(@NonNull String parentId, @NonNull Result> result) { List mediaItems= new ArrayList<>(); MediaDescriptionCompat mediaDescriptionCompat = new MediaDescriptionCompat.Builder().setDescription("setDescription").setMediaId("1L").setTitle("Title").build(); mediaItems.add(new MediaBrowserCompat.MediaItem(mediaDescriptionCompat, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)); result.sendResult(mediaItems); }*/ @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { // Check if the app is on background, then starts a foreground service and then ends it right after onStartForeground(); if (manager != null) { MediaButtonReceiver.handleIntent(manager.getMetadata().getSession(), intent); } return START_NOT_STICKY; } manager = new MusicManager(this); handler = new Handler(); super.onStartCommand(intent, flags, startId); return START_STICKY; } @Override public void onDestroy() { super.onDestroy(); destroy(); } @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); if (manager == null || manager.shouldStopWithApp()) { stopSelf(); } } } ``` **4.DefaultPlaybackController.java** ``` public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController { /** * The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; /** * The default rewind increment, in milliseconds. */ public static final int DEFAULT_REWIND_MS = 5000; private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP; protected final long rewindIncrementMs; protected final long fastForwardIncrementMs; /** * Creates a new instance. *

* Equivalent to {@code DefaultPlaybackController( * DefaultPlaybackController.DEFAULT_REWIND_MS, * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. */ public DefaultPlaybackController() { this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); } /** * Creates a new instance with the given fast forward and rewind increments. * * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will * cause the rewind action to be disabled. * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative * value will cause the fast forward action to be removed. */ public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { this.rewindIncrementMs = rewindIncrementMs; this.fastForwardIncrementMs = fastForwardIncrementMs; } @Override public long getSupportedPlaybackActions(Player player) { if (player == null || player.getCurrentTimeline().isEmpty()) { return 0; } long actions = BASE_ACTIONS; if (player.isCurrentWindowSeekable()) { actions |= PlaybackStateCompat.ACTION_SEEK_TO; } if (fastForwardIncrementMs > 0) { actions |= PlaybackStateCompat.ACTION_FAST_FORWARD; } if (rewindIncrementMs > 0) { actions |= PlaybackStateCompat.ACTION_REWIND; } return actions; } @Override public void onPlay(Player player) { player.setPlayWhenReady(true); } @Override public void onPause(Player player) { player.setPlayWhenReady(false); } @Override public void onSeekTo(Player player, long position) { long duration = player.getDuration(); if (duration != C.TIME_UNSET) { position = Math.min(position, duration); } player.seekTo(Math.max(position, 0)); } @Override public void onFastForward(Player player) { if (fastForwardIncrementMs <= 0) { return; } onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs); } @Override public void onRewind(Player player) { if (rewindIncrementMs <= 0) { return; } onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs); } @Override public void onStop(Player player) { player.stop(); } } ``` **5.MediaSessionConnector** ``` public final class MediaSessionConnector { static { ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); } public static final String EXTRAS_PITCH = "EXO_PITCH"; /** * Interface to which playback preparation actions are delegated. */ public interface PlaybackPreparer { long ACTIONS = PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH | PlaybackStateCompat.ACTION_PREPARE_FROM_URI | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH | PlaybackStateCompat.ACTION_PLAY_FROM_URI; /** * Returns the actions which are supported by the preparer. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, * {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, * {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and * {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}. * * @return The bitmask of the supported media actions. */ long getSupportedPrepareActions(); /** * See {@link MediaSessionCompat.Callback#onPrepare()}. */ void onPrepare(); /** * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */ void onPrepareFromMediaId(String mediaId, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */ void onPrepareFromSearch(String query, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ void onPrepareFromUri(Uri uri, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */ void onCommand(String command, Bundle extras, ResultReceiver cb); } /** * Interface to which playback actions are delegated. */ public interface PlaybackController { long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_STOP; /** * Returns the actions which are supported by the controller. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}. * * @param player The player. * @return The bitmask of the supported media actions. */ long getSupportedPlaybackActions(@Nullable Player player); /** * See {@link MediaSessionCompat.Callback#onPlay()}. */ void onPlay(Player player); /** * See {@link MediaSessionCompat.Callback#onPause()}. */ void onPause(Player player); /** * See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */ void onSeekTo(Player player, long position); /** * See {@link MediaSessionCompat.Callback#onFastForward()}. */ void onFastForward(Player player); /** * See {@link MediaSessionCompat.Callback#onRewind()}. */ void onRewind(Player player); /** * See {@link MediaSessionCompat.Callback#onStop()}. */ void onStop(Player player); } /** * Handles queue navigation actions, and updates the media session queue by calling * {@code MediaSessionCompat.setQueue()}. */ public interface QueueNavigator { long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; /** * Returns the actions which are supported by the navigator. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}, * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. * * @param player The {@link Player}. * @return The bitmask of the supported media actions. */ long getSupportedQueueNavigatorActions(@Nullable Player player); /** * Called when the timeline of the player has changed. * * @param player The player of which the timeline has changed. */ void onTimelineChanged(Player player); /** * Called when the current window index changed. * * @param player The player of which the current window index of the timeline has changed. */ void onCurrentWindowIndexChanged(Player player); /** * Gets the id of the currently active queue item, or * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. *

* To let the connector publish metadata for the active queue item, the queue item with the * returned id must be available in the list of items returned by * {@link MediaControllerCompat#getQueue()}. * * @param player The player connected to the media session. * @return The id of the active queue item. */ long getActiveQueueItemId(@Nullable Player player); /** * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */ void onSkipToPrevious(Player player); /** * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */ void onSkipToQueueItem(Player player, long id); /** * See {@link MediaSessionCompat.Callback#onSkipToNext()}. */ void onSkipToNext(Player player); /** * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}. */ void onSetShuffleModeEnabled(Player player, boolean enabled); } /** * Handles media session queue edits. */ public interface QueueEditor { long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING; /** * Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does * not declare action constants for adding and removing queue items. * * @param player The {@link Player}. */ long getSupportedQueueEditorActions(@Nullable Player player); /** * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}. */ void onAddQueueItem(Player player, MediaDescriptionCompat description); /** * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, * int index)}. */ void onAddQueueItem(Player player, MediaDescriptionCompat description, int index); /** * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat * description)}. */ void onRemoveQueueItem(Player player, MediaDescriptionCompat description); /** * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */ void onRemoveQueueItemAt(Player player, int index); /** * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */ void onSetRating(Player player, RatingCompat rating); } /** * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when * sent by a media controller. */ public interface CustomActionProvider { /** * Called when a custom action provided by this provider is sent to the media session. * * @param action The name of the action which was sent by a media controller. * @param extras Optional extras sent by a media controller. */ void onCustomAction(String action, Bundle extras); /** * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the * media session by the connector or {@code null} if this action should not be published at the * given player state. * * @return The custom action to be included in the session playback state or {@code null}. */ PlaybackStateCompat.CustomAction getCustomAction(); } /** * Converts an exception into an error code and a user readable error message. */ public interface ErrorMessageProvider { /** * Returns a pair consisting of an error code and a user readable error message for a given * exception. */ Pair getErrorMessage(ExoPlaybackException playbackException); } /** * The wrapped {@link MediaSessionCompat}. */ public final MediaSessionCompat mediaSession; private final MediaControllerCompat mediaController; private final Handler handler; private final boolean doMaintainMetadata; private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; private Player player; private CustomActionProvider[] customActionProviders; private int currentWindowIndex; private Map customActionMap; private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; private QueueNavigator queueNavigator; private QueueEditor queueEditor; private ExoPlaybackException playbackException; /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

* Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. */ public MediaSessionConnector(MediaSessionCompat mediaSession) { this(mediaSession, new DefaultPlaybackController()); } /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

* Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController) { this(mediaSession, playbackController, true); } /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController, boolean doMaintainMetadata) { this.mediaSession = mediaSession; this.playbackController = playbackController; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); customActionMap = Collections.emptyMap(); } /** * Sets the player to be connected to the media session. *

* The order in which any {@link CustomActionProvider}s are passed determines the order of the * actions published with the playback state of the session. * * @param player The player to be connected to the {@code MediaSession}. * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle * custom actions. */ public void setPlayer(Player player, PlaybackPreparer playbackPreparer, CustomActionProvider... customActionProviders) { if (this.player != null) { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } this.playbackPreparer = playbackPreparer; this.player = player; this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; if (player != null) { mediaSession.setCallback(mediaSessionCallback, handler); player.addListener(exoPlayerEventListener); } updateMediaSessionPlaybackState(); updateMediaSessionMetadata(); } /** * Sets the {@link ErrorMessageProvider}. * * @param errorMessageProvider The error message provider. */ public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; } /** * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT}, * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. * * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { this.queueNavigator = queueNavigator; } /** * Sets the {@link QueueEditor} to handle queue edits sent by the media controller. * * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; } private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); return; } Map currentActions = new HashMap<>(); for (CustomActionProvider customActionProvider : customActionProviders) { PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(); if (customAction != null) { currentActions.put(customAction.getAction(), customActionProvider); builder.addCustomAction(customAction); } } customActionMap = Collections.unmodifiableMap(currentActions); int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); if (playbackException != null) { if (errorMessageProvider != null) { Pair message = errorMessageProvider.getErrorMessage(playbackException); builder.setErrorMessage(message.first, message.second); } if (player.getPlaybackState() != Player.STATE_IDLE) { playbackException = null; } } long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) : MediaSessionCompat.QueueItem.UNKNOWN_ID; Bundle extras = new Bundle(); extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch); builder.setActions(buildPlaybackActions()) .setActiveQueueItemId(activeQueueItemId) .setBufferedPosition(player.getBufferedPosition()) .setState(sessionPlaybackState, player.getCurrentPosition(), player.getPlaybackParameters().speed, SystemClock.elapsedRealtime()) .setExtras(extras); mediaSession.setPlaybackState(builder.build()); } private long buildPlaybackActions() { long actions = 0; if (playbackController != null) { actions |= (PlaybackController.ACTIONS & playbackController .getSupportedPlaybackActions(player)); } if (playbackPreparer != null) { actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); } if (queueNavigator != null) { actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions( player)); } if (queueEditor != null) { actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player)); } return actions; } private void updateMediaSessionMetadata() { if (doMaintainMetadata) { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); if (player != null && player.isPlayingAd()) { builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1); } builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0 : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration()); if (queueNavigator != null) { long activeQueueItemId = queueNavigator.getActiveQueueItemId(player); List queue = mediaController.getQueue(); for (int i = 0; queue != null && i < queue.size(); i++) { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); if (description.getTitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, String.valueOf(description.getTitle())); } if (description.getSubtitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(description.getSubtitle())); } if (description.getDescription() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, String.valueOf(description.getDescription())); } if (description.getIconBitmap() != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap()); } if (description.getIconUri() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(description.getIconUri())); } if (description.getMediaId() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, String.valueOf(description.getMediaId())); } if (description.getMediaUri() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(description.getMediaUri())); } break; } } } mediaSession.setMetadata(builder.build()); } } private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) { switch (exoPlayerPlaybackState) { case Player.STATE_BUFFERING: return PlaybackStateCompat.STATE_BUFFERING; case Player.STATE_READY: return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; case Player.STATE_ENDED: return PlaybackStateCompat.STATE_PAUSED; default: return PlaybackStateCompat.STATE_NONE; } } private boolean canDispatchToPlaybackPreparer(long action) { return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0; } private boolean canDispatchToPlaybackController(long action) { return playbackController != null && (playbackController.getSupportedPlaybackActions(player) & PlaybackController.ACTIONS & action) != 0; } private boolean canDispatchToQueueNavigator(long action) { return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player) & QueueNavigator.ACTIONS & action) != 0; } private boolean canDispatchToQueueEditor(long action) { return queueEditor != null && (queueEditor.getSupportedQueueEditorActions(player) & QueueEditor.ACTIONS & action) != 0; } private class ExoPlayerEventListener implements Player.EventListener { // // @Override // public void onTimelineChanged(Timeline timeline, Object manifest) { // if (queueNavigator != null) { // queueNavigator.onTimelineChanged(player); // } // currentWindowIndex = player.getCurrentWindowIndex(); // updateMediaSessionMetadata(); // } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // Do nothing. } @Override public void onLoadingChanged(boolean isLoading) { // Do nothing. } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateMediaSessionPlaybackState(); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL ? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE); updateMediaSessionPlaybackState(); } @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; updateMediaSessionPlaybackState(); } // @Override // public void onPositionDiscontinuity() { // if (currentWindowIndex != player.getCurrentWindowIndex()) { // if (queueNavigator != null) { // queueNavigator.onCurrentWindowIndexChanged(player); // } // updateMediaSessionMetadata(); // currentWindowIndex = player.getCurrentWindowIndex(); // } // updateMediaSessionPlaybackState(); // } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { updateMediaSessionPlaybackState(); } } private class MediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPlay() { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) { playbackController.onPlay(player); } } @Override public void onPause() { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) { playbackController.onPause(player); } } @Override public void onSeekTo(long position) { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) { playbackController.onSeekTo(player, position); } } @Override public void onFastForward() { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) { playbackController.onFastForward(player); } } @Override public void onRewind() { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) { playbackController.onRewind(player); } } @Override public void onStop() { if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) { playbackController.onStop(player); } } @Override public void onSkipToNext() { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { queueNavigator.onSkipToNext(player); } } @Override public void onSkipToPrevious() { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) { queueNavigator.onSkipToPrevious(player); } } @Override public void onSkipToQueueItem(long id) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) { queueNavigator.onSkipToQueueItem(player, id); } } @Override public void onSetRepeatMode(int repeatMode) { // implemented as custom action } @Override public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { Map actionMap = customActionMap; if (actionMap.containsKey(action)) { actionMap.get(action).onCustomAction(action, extras); updateMediaSessionPlaybackState(); } } @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { if (playbackPreparer != null) { playbackPreparer.onCommand(command, extras, cb); } } @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { player.stop(); player.setPlayWhenReady(false); playbackPreparer.onPrepare(); } } @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { player.stop(); player.setPlayWhenReady(false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { player.stop(); player.setPlayWhenReady(false); playbackPreparer.onPrepareFromSearch(query, extras); } } @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { player.stop(); player.setPlayWhenReady(false); playbackPreparer.onPrepareFromUri(uri, extras); } } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { player.stop(); player.setPlayWhenReady(true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { player.stop(); player.setPlayWhenReady(true); playbackPreparer.onPrepareFromSearch(query, extras); } } @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { player.stop(); player.setPlayWhenReady(true); playbackPreparer.onPrepareFromUri(uri, extras); } } // @Override // public void onSetShuffleModeEnabled(boolean enabled) { // if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { // queueNavigator.onSetShuffleModeEnabled(player, enabled); // } // } @Override public void onAddQueueItem(MediaDescriptionCompat description) { if (queueEditor != null) { queueEditor.onAddQueueItem(player, description); } } @Override public void onAddQueueItem(MediaDescriptionCompat description, int index) { if (queueEditor != null) { queueEditor.onAddQueueItem(player, description, index); } } @Override public void onRemoveQueueItem(MediaDescriptionCompat description) { if (queueEditor != null) { queueEditor.onRemoveQueueItem(player, description); } } @Override public void onRemoveQueueItemAt(int index) { if (queueEditor != null) { queueEditor.onRemoveQueueItemAt(player, index); } } @Override public void onSetRating(RatingCompat rating) { if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) { queueEditor.onSetRating(player, rating); } } } } ``` **6. TimelineQueueNavigator** ``` public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; public static final int DEFAULT_MAX_QUEUE_SIZE = 10; private final MediaSessionCompat mediaSession; protected final int maxQueueSize; private long activeQueueItemId; /** * Creates an instance for a given {@link MediaSessionCompat}. *

* Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. * * @param mediaSession The {@link MediaSessionCompat}. */ public TimelineQueueNavigator(MediaSessionCompat mediaSession) { this(mediaSession, DEFAULT_MAX_QUEUE_SIZE); } /** * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size. *

* If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize}, * the media session queue will correspond to {@code maxQueueSize} windows centered on the one * currently being played. * * @param mediaSession The {@link MediaSessionCompat}. * @param maxQueueSize The maximum queue size. */ public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) { this.mediaSession = mediaSession; this.maxQueueSize = maxQueueSize; activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; } /** * Gets the {@link MediaDescriptionCompat} for a given timeline window index. * * @param windowIndex The timeline window index for which to provide a description. * @return A {@link MediaDescriptionCompat}. */ public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); @Override public long getSupportedQueueNavigatorActions(Player player) { if (player == null || player.getCurrentTimeline().getWindowCount() < 2) { return 0; } if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) { return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; } int currentWindowIndex = player.getCurrentWindowIndex(); long actions; if (currentWindowIndex == 0) { actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT; } else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) { actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } else { actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; } @Override public final void onTimelineChanged(Player player) { publishFloatingQueueWindow(player); } @Override public final void onCurrentWindowIndexChanged(Player player) { if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { publishFloatingQueueWindow(player); } else if (!player.getCurrentTimeline().isEmpty()) { activeQueueItemId = player.getCurrentWindowIndex(); } } @Override public final long getActiveQueueItemId(@Nullable Player player) { return activeQueueItemId; } @Override public void onSkipToPrevious(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; } int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), player.getRepeatMode(), player.getShuffleModeEnabled()); if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS || previousWindowIndex == C.INDEX_UNSET) { player.seekTo(0); } else { player.seekTo(previousWindowIndex, C.TIME_UNSET); } } @Override public void onSkipToQueueItem(Player player, long id) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; } int windowIndex = (int) id; if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) { player.seekTo(windowIndex, C.TIME_UNSET); } } @Override public void onSkipToNext(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; } int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), player.getRepeatMode(), player.getShuffleModeEnabled()); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); } } @Override public void onSetShuffleModeEnabled(Player player, boolean enabled) { // TODO: Implement this. } private void publishFloatingQueueWindow(Player player) { if (player.getCurrentTimeline().isEmpty()) { mediaSession.setQueue(Collections.emptyList()); activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; return; } int windowCount = player.getCurrentTimeline().getWindowCount(); int currentWindowIndex = player.getCurrentWindowIndex(); int queueSize = Math.min(maxQueueSize, windowCount); int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, windowCount - queueSize); List queue = new ArrayList<>(); for (int i = startIndex; i < startIndex + queueSize; i++) { queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i)); } mediaSession.setQueue(queue); activeQueueItemId = currentWindowIndex; } } ``` **7.LocalPlayback.java** ``` public class LocalPlayback extends ExoPlayback { private final long cacheMaxSize; private SimpleCache cache; private ConcatenatingMediaSource source; private boolean prepared = false; public LocalPlayback(Context context, MusicManager manager, SimpleExoPlayer player, long maxCacheSize) { super(context, manager, player); this.cacheMaxSize = maxCacheSize; } private void attachMediaSession(MusicManager manager, SimpleExoPlayer player, MediaSource trackSource) { MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(manager.getMetadata().getSession()); mediaSessionConnector.setPlayer(player, new MediaSessionConnector.PlaybackPreparer() { @Override public long getSupportedPrepareActions() { long actions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; if (player.getPlayWhenReady()) { actions |= PlaybackStateCompat.ACTION_PAUSE; } return actions; // return manager.getMetadata().getSession().getController().getPlaybackState().getActions(); } @Override public void onPrepare() { Log.e("LocalPlayback", "onPrepare"); } @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { // mediaSessionConnector.mediaSession.setMetadata(); Log.e("LocalPlayback", "onPrepareFromMediaId"); } @Override public void onPrepareFromSearch(String query, Bundle extras) { Log.e("LocalPlayback", "onPrepareFromSearch"); } @Override public void onPrepareFromUri(Uri uri, Bundle extras) { Log.e("LocalPlayback", "onPrepareFromUri"); } @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { Log.e("LocalPlayback", "onCommand"); } }); mediaSessionConnector.setQueueNavigator(new TimelineQueueNavigator(manager.getMetadata().getSession()) { @Override public MediaDescriptionCompat getMediaDescription(int windowIndex) { return manager.getMetadata().getSession().getController().getQueue().get(windowIndex).getDescription(); } }); /* // Create media sources. MediaSource[] mediaSources = new MediaSource[getQueue().size()]; for (int i = 0; i < getQueue().size(); i++) { // mediaSources[i] = createMediaSource(manager.getMetadata().getSession().getController().getQueue().get(i)); mediaSources[i] = buildMediaSource(getQueue().get(i).uri); } player.prepare(new ConcatenatingMediaSource(mediaSources));*/ // mediaSessionConnector.setErrorMessageProvider(playbackException -> new Pair<>(1, playbackException.getLocalizedMessage())); } private MediaSource createMediaSource(MediaSessionCompat.QueueItem queueItem) { return buildMediaSource(queueItem.getDescription().getMediaUri()); } private MediaSource buildMediaSource(Uri uri) { // Create a data source factory. DataSource.Factory dataSourceFactory = new DefaultHttpDataSourceFactory(Util.getUserAgent(context, "futurimobile" + BuildConfig.VERSION_NAME)); // Create a SmoothStreaming media source pointing to a manifest uri. return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); /* return new ExtractorMediaSource.Factory( new DefaultHttpDataSourceFactory("audio_streaming")). createMediaSource(uri);*/ } @Override public void initialize() { if (cacheMaxSize > 0) { File cacheDir = new File(context.getCacheDir(), "TrackPlayer"); DatabaseProvider db = new ExoDatabaseProvider(context); cache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(cacheMaxSize), db); } else { cache = null; } super.initialize(); resetQueue(); } public DataSource.Factory enableCaching(DataSource.Factory ds) { if (cache == null || cacheMaxSize <= 0) return ds; return new CacheDataSourceFactory(cache, ds, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); } private void prepare() { if (!prepared) { Log.d(Utils.LOG, "Preparing the media source..."); // Create media sources. MediaSource[] mediaSources = new MediaSource[getQueue().size()]; for (int i = 0; i < getQueue().size(); i++) { // mediaSources[i] = createMediaSource(manager.getMetadata().getSession().getController().getQueue().get(i)); mediaSources[i] = buildMediaSource(getQueue().get(i).uri); } player.prepare(new ConcatenatingMediaSource(mediaSources)); // player.prepare(source, false, false); prepared = true; } } @Override public void add(Track track, int index, Promise promise) { queue.add(index, track); MediaSource trackSource = track.toMediaSource(context, this); source.addMediaSource(index, trackSource, manager.getHandler(), Utils.toRunnable(promise)); prepare(); attachMediaSession(manager, player, source); } @Override public void add(Collection tracks, int index, Promise promise) { List trackList = new ArrayList<>(); for (Track track : tracks) { trackList.add(track.toMediaSource(context, this)); } queue.addAll(index, tracks); source.addMediaSources(index, trackList, manager.getHandler(), Utils.toRunnable(promise)); prepare(); } @Override public void remove(List indexes, Promise promise) { int currentIndex = player.getCurrentWindowIndex(); // Sort the list so we can loop through sequentially Collections.sort(indexes); for (int i = indexes.size() - 1; i >= 0; i--) { int index = indexes.get(i); // Skip indexes that are the current track or are out of bounds if (index == currentIndex || index < 0 || index >= queue.size()) { // Resolve the promise when the last index is invalid if (i == 0) promise.resolve(null); continue; } queue.remove(index); if (i == 0) { source.removeMediaSource(index, manager.getHandler(), Utils.toRunnable(promise)); } else { source.removeMediaSource(index); } } } @Override public void removeUpcomingTracks() { int currentIndex = player.getCurrentWindowIndex(); if (currentIndex == C.INDEX_UNSET) return; for (int i = queue.size() - 1; i > currentIndex; i--) { queue.remove(i); source.removeMediaSource(i); } } private void resetQueue() { queue.clear(); source = new ConcatenatingMediaSource(); player.prepare(source, true, true); prepared = false; // We set it to false as the queue is now empty lastKnownWindow = C.INDEX_UNSET; lastKnownPosition = C.POSITION_UNSET; manager.onReset(); } @Override public void play() { prepare(); super.play(); } @Override public void stop() { super.stop(); prepared = false; } @Override public void seekTo(long time) { prepare(); super.seekTo(time); } @Override public void reset() { Track track = getCurrentTrack(); long position = player.getCurrentPosition(); super.reset(); resetQueue(); manager.onTrackUpdate(track, position, null); } @Override public float getPlayerVolume() { return player.getVolume(); } @Override public void setPlayerVolume(float volume) { player.setVolume(volume); } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED) { prepared = false; } super.onPlayerStateChanged(playWhenReady, playbackState); } @Override public void onPlayerError(ExoPlaybackException error) { prepared = false; super.onPlayerError(error); } @Override public void destroy() { super.destroy(); if (cache != null) { try { cache.release(); cache = null; } catch (Exception ex) { Log.w(Utils.LOG, "Couldn't release the cache properly", ex); } } } } ```

Guichaguri commented 5 years ago

Android Auto requires a MediaBrowserService, we can't have that as the way React Native works, so it's not supported.

The only way to achieve that is to create a new MediaBrowserService and copying all the code from HeadlessJsTaskService into the module. We should always avoid playing directly with RN's internals and even because that would require too much work to maintain through all versions.