Introduce MediaSessionConnector.CommandReceiver interface and add TimelineQueueEditor.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=166475351
This commit is contained in:
bachinger 2017-08-25 07:43:15 -07:00 committed by Oliver Woodman
parent 1b9c904dba
commit 01f4819844
4 changed files with 316 additions and 22 deletions

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
@ -125,4 +127,14 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
player.stop();
}
@Override
public String[] getCommands() {
return null;
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
// Do nothing.
}
}

View File

@ -79,10 +79,24 @@ public final class MediaSessionConnector {
private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
| MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
/**
* Receiver of media commands sent by a media controller.
*/
public interface CommandReceiver {
/**
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
*/
String[] getCommands();
/**
* See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
*/
void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
}
/**
* Interface to which playback preparation actions are delegated.
*/
public interface PlaybackPreparer {
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_PREPARE
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
@ -121,16 +135,12 @@ public final class MediaSessionConnector {
* 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 {
public interface PlaybackController extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
@ -178,7 +188,7 @@ public final class MediaSessionConnector {
* Handles queue navigation actions, and updates the media session queue by calling
* {@code MediaSessionCompat.setQueue()}.
*/
public interface QueueNavigator {
public interface QueueNavigator extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
@ -240,7 +250,7 @@ public final class MediaSessionConnector {
/**
* Handles media session queue edits.
*/
public interface QueueEditor {
public interface QueueEditor extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
@ -309,6 +319,7 @@ public final class MediaSessionConnector {
private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback;
private final PlaybackController playbackController;
private final Map<String, CommandReceiver> commandMap;
private Player player;
private CustomActionProvider[] customActionProviders;
@ -328,7 +339,7 @@ public final class MediaSessionConnector {
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
this(mediaSession, new DefaultPlaybackController());
this(mediaSession, null);
}
/**
@ -350,7 +361,8 @@ public final class MediaSessionConnector {
* 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 playbackController A {@link PlaybackController} for handling playback actions, or
* {@code null} if the connector should handle playback actions directly.
* @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).
@ -358,7 +370,8 @@ public final class MediaSessionConnector {
public MediaSessionConnector(MediaSessionCompat mediaSession,
PlaybackController playbackController, boolean doMaintainMetadata) {
this.mediaSession = mediaSession;
this.playbackController = playbackController;
this.playbackController = playbackController != null ? playbackController
: new DefaultPlaybackController();
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
: Looper.getMainLooper());
this.doMaintainMetadata = doMaintainMetadata;
@ -367,6 +380,8 @@ public final class MediaSessionConnector {
mediaSessionCallback = new MediaSessionCallback();
exoPlayerEventListener = new ExoPlayerEventListener();
customActionMap = Collections.emptyMap();
commandMap = new HashMap<>();
registerCommandReceiver(playbackController);
}
/**
@ -386,8 +401,12 @@ public final class MediaSessionConnector {
this.player.removeListener(exoPlayerEventListener);
mediaSession.setCallback(null);
}
this.playbackPreparer = playbackPreparer;
unregisterCommandReceiver(this.playbackPreparer);
this.player = player;
this.playbackPreparer = playbackPreparer;
registerCommandReceiver(playbackPreparer);
this.customActionProviders = (player != null && customActionProviders != null)
? customActionProviders : new CustomActionProvider[0];
if (player != null) {
@ -416,7 +435,9 @@ public final class MediaSessionConnector {
* @param queueNavigator The queue navigator.
*/
public void setQueueNavigator(QueueNavigator queueNavigator) {
unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator;
registerCommandReceiver(queueNavigator);
}
/**
@ -425,11 +446,29 @@ public final class MediaSessionConnector {
* @param queueEditor The queue editor.
*/
public void setQueueEditor(QueueEditor queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
registerCommandReceiver(queueEditor);
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
: EDITOR_MEDIA_SESSION_FLAGS);
}
private void registerCommandReceiver(CommandReceiver commandReceiver) {
if (commandReceiver != null && commandReceiver.getCommands() != null) {
for (String command : commandReceiver.getCommands()) {
commandMap.put(command, commandReceiver);
}
}
}
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
if (commandReceiver != null && commandReceiver.getCommands() != null) {
for (String command : commandReceiver.getCommands()) {
commandMap.remove(command);
}
}
}
private void updateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
if (player == null) {
@ -473,11 +512,8 @@ public final class MediaSessionConnector {
}
private long buildPlaybackActions() {
long actions = 0;
if (playbackController != null) {
actions |= (PlaybackController.ACTIONS & playbackController
.getSupportedPlaybackActions(player));
}
long actions = (PlaybackController.ACTIONS
& playbackController.getSupportedPlaybackActions(player));
if (playbackPreparer != null) {
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
}
@ -562,7 +598,7 @@ public final class MediaSessionConnector {
}
private boolean canDispatchToPlaybackController(long action) {
return playbackController != null && (playbackController.getSupportedPlaybackActions(player)
return (playbackController.getSupportedPlaybackActions(player)
& PlaybackController.ACTIONS & action) != 0;
}
@ -583,10 +619,15 @@ public final class MediaSessionConnector {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
queueNavigator.onTimelineChanged(player);
updateMediaSessionPlaybackState();
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
// active queue item and queue navigation actions may need to be updated
updateMediaSessionPlaybackState();
}
int windowCount = player.getCurrentTimeline().getWindowCount();
if (currentWindowCount != windowCount) {
// active queue item and queue navigation actions may need to be updated
updateMediaSessionPlaybackState();
@ -638,8 +679,8 @@ public final class MediaSessionConnector {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
}
updateMediaSessionMetadata();
currentWindowIndex = player.getCurrentWindowIndex();
updateMediaSessionMetadata();
}
updateMediaSessionPlaybackState();
}
@ -732,8 +773,9 @@ public final class MediaSessionConnector {
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
if (playbackPreparer != null) {
playbackPreparer.onCommand(command, extras, cb);
CommandReceiver commandReceiver = commandMap.get(command);
if (commandReceiver != null) {
commandReceiver.onCommand(player, command, extras, cb);
}
}

View File

@ -0,0 +1,226 @@
/*
* Copyright (c) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
* A {@link MediaSessionConnector.QueueEditor} implementation based on the
* {@link DynamicConcatenatingMediaSource}.
* <p>
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
* This allows to move the currently playing window without interrupting playback.
*/
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
MediaSessionConnector.CommandReceiver {
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
public static final String EXTRA_FROM_INDEX = "from_index";
public static final String EXTRA_TO_INDEX = "to_index";
/**
* Factory to create {@link MediaSource}s.
*/
public interface MediaSourceFactory {
/**
* Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.
*
* @param description The {@link MediaDescriptionCompat} to create a media source for.
* @return A {@link MediaSource} or {@code null} if no source can be created for the given
* description.
*/
@Nullable MediaSource createMediaSource(MediaDescriptionCompat description);
}
/**
* Adapter to get {@link MediaDescriptionCompat} of items in the queue and to notify the
* application about changes in the queue to sync the data structure backing the
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
/**
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
*
* @param position The position in the queue for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*
* @param position The position at which to add.
* @param description The {@link MediaDescriptionCompat} to be added.
*/
void add(int position, MediaDescriptionCompat description);
/**
* Removes the item at the given {@code position}.
*
* @param position The position at which to remove the item.
*/
void remove(int position);
/**
* Moves a queue item from position {@code from} to position {@code to}.
*
* @param from The position from which to remove the item.
* @param to The target position to which to move the item.
*/
void move(int from, int to);
}
/**
* Used to evaluate whether two {@link MediaDescriptionCompat} are considered equal.
*/
interface MediaDescriptionEqualityChecker {
/**
* Returns {@code true} whether the descriptions are considered equal.
*
* @param d1 The first {@link MediaDescriptionCompat}.
* @param d2 The second {@link MediaDescriptionCompat}.
* @return {@code true} if considered equal.
*/
boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2);
}
/**
* Media description comparator comparing the media IDs. Media IDs are considered equals if both
* are {@code null}.
*/
public static final class MediaIdEqualityChecker implements MediaDescriptionEqualityChecker {
@Override
public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
return Util.areEqual(d1.getMediaId(), d2.getMediaId());
}
}
private final MediaControllerCompat mediaController;
private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory;
private final MediaDescriptionEqualityChecker equalityChecker;
private final DynamicConcatenatingMediaSource queueMediaSource;
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
* manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
*/
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker());
}
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
* manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;
this.queueDataAdapter = queueDataAdapter;
this.sourceFactory = sourceFactory;
this.equalityChecker = equalityChecker;
}
@Override
public long getSupportedQueueEditorActions(@Nullable Player player) {
return 0;
}
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description) {
onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount());
}
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);
}
}
@Override
public void onRemoveQueueItem(Player player, MediaDescriptionCompat description) {
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
for (int i = 0; i < queue.size(); i++) {
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
onRemoveQueueItemAt(player, i);
return;
}
}
}
@Override
public void onRemoveQueueItemAt(Player player, int index) {
queueDataAdapter.remove(index);
queueMediaSource.removeMediaSource(index);
}
@Override
public void onSetRating(Player player, RatingCompat rating) {
// Do nothing.
}
// CommandReceiver implementation.
@NonNull
@Override
public String[] getCommands() {
return new String[] {COMMAND_MOVE_QUEUE_ITEM};
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to);
queueMediaSource.moveMediaSource(from, to);
}
}
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
@ -164,6 +166,18 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL);
}
// CommandReceiver implementation.
@Override
public String[] getCommands() {
return null;
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
// Do nothing.
}
private void publishFloatingQueueWindow(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());