diff --git a/core_settings.gradle b/core_settings.gradle
index 31547f60df..20e7b235a2 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -28,6 +28,7 @@ include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
+include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
@@ -44,6 +45,7 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
+project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md
new file mode 100644
index 0000000000..7515cf9eef
--- /dev/null
+++ b/extensions/mediasession/README.md
@@ -0,0 +1,27 @@
+# ExoPlayer MediaSession Extension #
+
+## Description ##
+
+The MediaSession Extension mediates between an ExoPlayer instance and a
+[MediaSession][]. It automatically retrieves and implements playback actions
+and syncs the player state with the state of the media session. The behaviour
+can be extended to support other playback and custom actions.
+
+[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html
+
+## Getting the extension ##
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+compile 'com.google.android.exoplayer:extension-mediasession:rX.X.X'
+```
+
+where `rX.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
new file mode 100644
index 0000000000..c439543967
--- /dev/null
+++ b/extensions/mediasession/build.gradle
@@ -0,0 +1,42 @@
+// 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.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+}
+
+dependencies {
+ compile project(':library-core')
+ compile 'com.android.support:support-media-compat:' + supportLibraryVersion
+ compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
+}
+
+ext {
+ javadocTitle = 'Media session extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-mediasession'
+ releaseDescription = 'Media session extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/mediasession/src/main/AndroidManifest.xml b/extensions/mediasession/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..8ed6ef2011
--- /dev/null
+++ b/extensions/mediasession/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
new file mode 100644
index 0000000000..e023d0a983
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -0,0 +1,857 @@
+/*
+ * 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.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mediates between a {@link MediaSessionCompat} and an {@link SimpleExoPlayer} instance set with
+ * {@link #setPlayer(SimpleExoPlayer)}.
+ *
+ * By default the {@code MediaSessionConnector} listens for {@link #DEFAULT_PLAYBACK_ACTIONS} sent
+ * by a media controller and realizes these actions by calling appropriate ExoPlayer methods.
+ * Further, the state of ExoPlayer will be synced automatically with the {@link PlaybackStateCompat}
+ * of the media session to broadcast state transitions to clients. You can optionally extend this
+ * behaviour by providing various collaborators.
+ *
+ * Media actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
+ * {@code PlaybackStateCompat#ACTION_PLAY_*} need to be handled by a {@link PlaybackPreparer} which
+ * build a {@link com.google.android.exoplayer2.source.MediaSource} to prepare ExoPlayer. Deploy
+ * your preparer by calling {@link #setPlaybackPreparer(PlaybackPreparer)}.
+ *
+ * To support a media session queue and navigation within this queue, you can set a
+ * {@link QueueNavigator} to maintain the queue yourself and implement queue navigation commands
+ * (like 'skip to next') sent by controllers. It's recommended to use the
+ * {@link TimelineQueueNavigator} to allow users navigating the windows of the ExoPlayer timeline.
+ *
+ * If you want to allow media controllers to manipulate the queue, implement a {@link QueueEditor}
+ * and deploy it with {@link #setQueueEditor(QueueEditor)}.
+ *
+ * Set an {@link ErrorMessageProvider} to provide an error code and a human readable error message
+ * to be broadcast to controllers.
+ */
+public final class MediaSessionConnector {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
+ }
+
+ /**
+ * Actions that are published to the media session by default
+ * ({@code PlaybackStateCompat.ACTION_PLAY_PAUSE}, {@code PlaybackStateCompat.ACTION_PLAY},
+ * {@code PlaybackStateCompat.ACTION_PAUSE}, {@code PlaybackStateCompat.ACTION_FAST_FORWARD},
+ * {@code PlaybackStateCompat.ACTION_REWIND}).
+ */
+ public static final long DEFAULT_PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+
+ public static final String EXTRAS_PITCH = "EXO_PITCH";
+
+ public static final long DEFAULT_FAST_FORWARD_MS = 15000;
+ public static final long DEFAULT_REWIND_MS = 5000;
+
+ /**
+ * Interface of components taking responsibility of a set of media session playback actions
+ * ({@code PlaybackStateCompat#ACTION_*}).
+ */
+ public interface PlaybackActionSupport {
+ /**
+ * Returns the bit mask of the playback actions supported by this component.
+ */
+ long getSupportedPlaybackActions();
+ }
+
+ /**
+ * Interface to which media controller commands regarding preparing playback for a given media
+ * clip are delegated to.
+ *
+ * Normally preparing playback includes preparing the player with a
+ * {@link com.google.android.exoplayer2.source.MediaSource} and setting up the media session queue
+ * with a corresponding list of queue items.
+ */
+ public interface PlaybackPreparer extends PlaybackActionSupport {
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepare()}.
+ */
+ void onPrepare(ExoPlayer player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
+ */
+ void onPrepareFromMediaId(ExoPlayer player, String mediaId, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
+ */
+ void onPrepareFromSearch(ExoPlayer player, String query, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
+ */
+ void onPrepareFromUri(ExoPlayer player, Uri uri, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
+ */
+ void onCommand(ExoPlayer player, String command, Bundle extras, ResultReceiver cb);
+ }
+
+ /**
+ * Navigator to handle queue navigation commands and maintain the media session queue with
+ * {#link MediaSessionCompat#setQueue(List)} to provide the active queue item to the connector.
+ */
+ public interface QueueNavigator extends PlaybackActionSupport {
+ /**
+ * Called when the timeline of the player has changed.
+ *
+ * @param player The player of which the timeline has changed.
+ */
+ void onTimelineChanged(ExoPlayer 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(ExoPlayer 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 ExoPlayer player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToPrevious()).
+ */
+ void onSkipToPrevious(ExoPlayer player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
+ */
+ void onSkipToQueueItem(ExoPlayer player, long id);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
+ */
+ void onSkipToNext(ExoPlayer player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}.
+ */
+ void onSetShuffleModeEnabled(ExoPlayer player, boolean enabled);
+ }
+
+ /**
+ * Editor to manipulate the queue.
+ */
+ public interface QueueEditor extends PlaybackActionSupport {
+ /**
+ * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.
+ */
+ void onAddQueueItem(ExoPlayer player, MediaDescriptionCompat description);
+ /**
+ * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
+ * int index)}.
+ */
+ void onAddQueueItem(ExoPlayer player, MediaDescriptionCompat description, int index);
+ /**
+ * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat
+ * description)}.
+ */
+ void onRemoveQueueItem(ExoPlayer player, MediaDescriptionCompat description);
+ /**
+ * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)).
+ */
+ void onRemoveQueueItemAt(ExoPlayer player, int index);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}.
+ */
+ void onSetRating(ExoPlayer 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 player The player for which to process the custom action.
+ * @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(SimpleExoPlayer player, 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.
+ *
+ * @param player The player for which to provide actions.
+ * @return The custom action to be included in the session playback state or {@code null}.
+ */
+ PlaybackStateCompat.CustomAction getCustomAction(SimpleExoPlayer player);
+ }
+
+ /**
+ * Provides an user readable error code and a message for {@link ExoPlaybackException}s.
+ */
+ public interface ErrorMessageProvider {
+ /**
+ * Returns a pair of an error code and a user readable error message for a given
+ * {@link ExoPlaybackException}.
+ */
+ 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 CustomActionProvider[] customActionProviders;
+
+ private SimpleExoPlayer player;
+ private int currentWindowIndex;
+ private long playbackActions;
+ private long fastForwardIncrementMs;
+ private long rewindIncrementMs;
+ private Map customActionMap;
+ private ErrorMessageProvider errorMessageProvider;
+ private PlaybackPreparer playbackPreparer;
+ private QueueNavigator queueNavigator;
+ private QueueEditor queueEditor;
+ private ExoPlaybackException playbackException;
+
+ /**
+ * Creates a {@code MediaSessionConnector}. This is equivalent to calling
+ * {@code #MediaSessionConnector(mediaSession, true)}.
+ *
+ * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as
+ * constructing the player instance.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ */
+ public MediaSessionConnector(MediaSessionCompat mediaSession) {
+ this(mediaSession, true);
+ }
+
+ /**
+ * Creates a {@code MediaSessionConnector} with {@link CustomActionProvider}s.
+ *
+ * The order in which the {@link CustomActionProvider}s are passed to the constructor determines
+ * the order of the actions published with the playback state of the session.
+ *
+ * If you choose to pass {@code false} for {@code doMaintainMetadata} you need to maintain the
+ * metadata of the media session yourself (provide at least the duration to allow clients to show
+ * a progress bar).
+ *
+ * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as
+ * constructing the player instance.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ * @param doMaintainMetadata Sets whether the connector should maintain the metadata of the
+ * session.
+ * @param customActionProviders {@link CustomActionProvider}s to publish and handle custom
+ * actions.
+ */
+ public MediaSessionConnector(MediaSessionCompat mediaSession, boolean doMaintainMetadata,
+ CustomActionProvider... customActionProviders) {
+ this.mediaSession = mediaSession;
+ this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
+ : Looper.getMainLooper());
+ this.doMaintainMetadata = doMaintainMetadata;
+ this.customActionProviders = customActionProviders != null ? customActionProviders
+ : new CustomActionProvider[0];
+ mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mediaController = mediaSession.getController();
+ mediaSessionCallback = new MediaSessionCallback();
+ exoPlayerEventListener = new ExoPlayerEventListener();
+ playbackActions = DEFAULT_PLAYBACK_ACTIONS;
+ customActionMap = Collections.emptyMap();
+ fastForwardIncrementMs = DEFAULT_FAST_FORWARD_MS;
+ rewindIncrementMs = DEFAULT_REWIND_MS;
+ }
+
+ /**
+ * Sets the player to which media commands sent by a media controller are delegated.
+ *
+ * The media session callback is set if the {@code player} is not {@code null} and the callback is
+ * removed if the {@code player} is {@code null}.
+ *
+ * @param player The player to be connected to the {@code MediaSession}.
+ */
+ public void setPlayer(SimpleExoPlayer player) {
+ if (this.player != null) {
+ this.player.removeListener(exoPlayerEventListener);
+ mediaSession.setCallback(null);
+ }
+ this.player = player;
+ if (player != null) {
+ mediaSession.setCallback(mediaSessionCallback, handler);
+ player.addListener(exoPlayerEventListener);
+ }
+ updateMediaSessionPlaybackState();
+ updateMediaSessionMetadata();
+ }
+
+ /**
+ * Sets the fast forward increment in milliseconds. A positive value will cause the
+ * {@link PlaybackStateCompat#ACTION_FAST_FORWARD} playback action to be added. A zero or a
+ * negative value will cause it to be removed.
+ *
+ * @param fastForwardIncrementMs The fast forward increment in milliseconds.
+ */
+ public void setFastForwardIncrementMs(long fastForwardIncrementMs) {
+ this.fastForwardIncrementMs = fastForwardIncrementMs;
+ if (fastForwardIncrementMs > 0) {
+ addPlaybackActions(PlaybackStateCompat.ACTION_FAST_FORWARD);
+ } else {
+ removePlaybackActions(PlaybackStateCompat.ACTION_FAST_FORWARD);
+ }
+ }
+
+ /**
+ * Sets the rewind increment in milliseconds. A positive value will cause the
+ * {@link PlaybackStateCompat#ACTION_REWIND} playback action to be added. A zero or a
+ * negative value will cause it to be removed.
+ *
+ * @param rewindIncrementMs The rewind increment in milliseconds.
+ */
+ public void setRewindIncrementMs(long rewindIncrementMs) {
+ this.rewindIncrementMs = rewindIncrementMs;
+ if (rewindIncrementMs > 0) {
+ addPlaybackActions(PlaybackStateCompat.ACTION_REWIND);
+ } else {
+ removePlaybackActions(PlaybackStateCompat.ACTION_REWIND);
+ }
+ }
+
+ /**
+ * Adds playback actions. The playback actions that are enabled by default are those in
+ * {@link MediaSessionConnector#DEFAULT_PLAYBACK_ACTIONS}. See {@link PlaybackStateCompat} for
+ * available playback action constants.
+ *
+ * @param playbackActions The playback actions to add.
+ */
+ public void addPlaybackActions(long playbackActions) {
+ this.playbackActions |= playbackActions;
+ }
+
+ /**
+ * Removes playback actions. The playback actions that are enabled by default are those in
+ * {@link MediaSessionConnector#DEFAULT_PLAYBACK_ACTIONS}.
+ *
+ * @param playbackActions The playback actions to remove.
+ */
+ public void removePlaybackActions(long playbackActions) {
+ this.playbackActions &= ~playbackActions;
+ }
+
+ /**
+ * Sets the playback actions. The playback actions that are enabled by default are overridden.
+ *
+ * @param playbackActions The playback actions to publish.
+ */
+ public void setPlaybackActions(long playbackActions) {
+ this.playbackActions = playbackActions;
+ }
+
+ /**
+ * Sets the optional {@link ErrorMessageProvider}.
+ *
+ * @param errorMessageProvider The {@link ErrorMessageProvider}.
+ */
+ public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) {
+ this.errorMessageProvider = errorMessageProvider;
+ }
+
+ /**
+ * Sets the {@link QueueNavigator} to handle queue navigation for the media 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 navigator to handle queue navigation.
+ */
+ public void setQueueNavigator(QueueNavigator queueNavigator) {
+ if (this.queueNavigator != null) {
+ removePlaybackActions(this.queueNavigator.getSupportedPlaybackActions());
+ }
+ this.queueNavigator = queueNavigator;
+ if (queueNavigator != null) {
+ addPlaybackActions(queueNavigator.getSupportedPlaybackActions());
+ }
+ }
+
+ /**
+ * Sets the queue editor to handle commands to manipulate the queue sent by a media controller.
+ *
+ * @param queueEditor The editor to handle queue manipulation actions.
+ */
+ public void setQueueEditor(QueueEditor queueEditor) {
+ if (this.queueEditor != null) {
+ removePlaybackActions(this.queueEditor.getSupportedPlaybackActions());
+ }
+ this.queueEditor = queueEditor;
+ if (queueEditor != null) {
+ addPlaybackActions(queueEditor.getSupportedPlaybackActions());
+ }
+ }
+
+ /**
+ * Sets the {@link PlaybackPreparer} to which preparation commands sent by a media
+ * controller are delegated.
+ *
+ * Required to work properly with Android Auto which requires
+ * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}.
+ *
+ * @param playbackPreparer The preparer to delegate to.
+ */
+ public void setPlaybackPreparer(PlaybackPreparer playbackPreparer) {
+ if (this.playbackPreparer != null) {
+ removePlaybackActions(this.playbackPreparer.getSupportedPlaybackActions());
+ }
+ this.playbackPreparer = playbackPreparer;
+ if (playbackPreparer != null) {
+ addPlaybackActions(playbackPreparer.getSupportedPlaybackActions());
+ }
+ }
+
+ private void updateMediaSessionPlaybackState() {
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ if (player == null) {
+ builder.setActions(0).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(player);
+ 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() != ExoPlayer.STATE_IDLE) {
+ playbackException = null;
+ }
+ }
+ long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
+ : MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ updatePlaybackActions(activeQueueItemId);
+ Bundle extras = new Bundle();
+ extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
+ builder.setActions(playbackActions)
+ .setActiveQueueItemId(activeQueueItemId)
+ .setBufferedPosition(player.getBufferedPosition())
+ .setState(sessionPlaybackState, player.getCurrentPosition(),
+ player.getPlaybackParameters().speed, SystemClock.elapsedRealtime())
+ .setExtras(extras);
+ mediaSession.setPlaybackState(builder.build());
+ }
+
+ private void updatePlaybackActions(long activeQueueItemId) {
+ List queue = mediaController.getQueue();
+ if (queue == null || queue.size() < 2) {
+ removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
+ } else if (player.getRepeatMode() != ExoPlayer.REPEAT_MODE_OFF) {
+ addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
+ } else if (activeQueueItemId == queue.get(0).getQueueId()) {
+ removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
+ addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
+ } else if (activeQueueItemId == queue.get(queue.size() - 1).getQueueId()) {
+ removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
+ addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
+ } else {
+ addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
+ }
+ }
+
+ 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 ExoPlayer.STATE_BUFFERING:
+ return PlaybackStateCompat.STATE_BUFFERING;
+ case ExoPlayer.STATE_READY:
+ return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
+ case ExoPlayer.STATE_ENDED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ default:
+ return PlaybackStateCompat.STATE_NONE;
+ }
+ }
+
+ private boolean isActionPublished(long action) {
+ return (playbackActions & action) != 0;
+ }
+
+ private boolean canDispatchToQueueNavigator(long action) {
+ return queueNavigator != null && isActionPublished(action);
+ }
+
+ private boolean canDispatchToPlaybackPreparer(long action) {
+ return playbackPreparer != null && isActionPublished(action);
+ }
+
+ private class ExoPlayerEventListener implements ExoPlayer.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(@ExoPlayer.RepeatMode int repeatMode) {
+ mediaSession.setRepeatMode(repeatMode == ExoPlayer.REPEAT_MODE_ONE
+ ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == ExoPlayer.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() {
+ player.setPlayWhenReady(true);
+ }
+
+ @Override
+ public void onPause() {
+ player.setPlayWhenReady(false);
+ }
+
+ @Override
+ public void onSeekTo(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() {
+ if (fastForwardIncrementMs <= 0) {
+ return;
+ }
+ onSeekTo(player.getCurrentPosition() + fastForwardIncrementMs);
+ }
+
+ @Override
+ public void onRewind() {
+ if (rewindIncrementMs <= 0) {
+ return;
+ }
+ onSeekTo(player.getCurrentPosition() - rewindIncrementMs);
+ }
+
+ @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 onStop() {
+ if (isActionPublished(PlaybackStateCompat.ACTION_STOP)) {
+ player.stop();
+ }
+ }
+
+ @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(player, action, extras);
+ updateMediaSessionPlaybackState();
+ }
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+ if (playbackPreparer != null) {
+ playbackPreparer.onCommand(player, command, extras, cb);
+ }
+ }
+
+ @Override
+ public void onPrepare() {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepare(player);
+ }
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromMediaId(player, mediaId, extras);
+ }
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromSearch(player, query, extras);
+ }
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
+ player.stop();
+ player.setPlayWhenReady(false);
+ playbackPreparer.onPrepareFromUri(player, 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(player, mediaId, extras);
+ }
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
+ player.stop();
+ player.setPlayWhenReady(true);
+ playbackPreparer.onPrepareFromSearch(player, query, extras);
+ }
+ }
+
+ @Override
+ public void onPlayFromUri(Uri uri, Bundle extras) {
+ if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
+ player.stop();
+ player.setPlayWhenReady(true);
+ playbackPreparer.onPrepareFromUri(player, 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 (queueEditor != null && isActionPublished(PlaybackStateCompat.ACTION_SET_RATING)) {
+ queueEditor.onSetRating(player, rating);
+ }
+ }
+
+ }
+
+}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
new file mode 100644
index 0000000000..3e453bdd28
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -0,0 +1,96 @@
+package com.google.android.exoplayer2.ext.mediasession;
+/*
+ * 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.
+ */
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
+
+/**
+ * Provides a custom action for toggling repeat actions.
+ */
+public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
+
+ private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
+
+ private final @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
+ private final CharSequence repeatAllDescription;
+ private final CharSequence repeatOneDescription;
+ private final CharSequence repeatOffDescription;
+
+ /**
+ * Creates a new {@link RepeatModeActionProvider}.
+ *
+ * This is equivalent to calling the two argument constructor with
+ * {@code RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
+ *
+ * @param context The context.
+ */
+ public RepeatModeActionProvider(Context context) {
+ this(context, RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL);
+ }
+
+ /**
+ * Creates a new {@link RepeatModeActionProvider} for the given repeat toggle modes.
+ *
+ * @param context The context.
+ * @param repeatToggleModes The toggle modes to enable.
+ */
+ public RepeatModeActionProvider(Context context,
+ @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
+ this.repeatToggleModes = repeatToggleModes;
+ repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
+ repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
+ repeatOffDescription = context.getString(R.string.exo_media_action_repeat_off_description);
+ }
+
+ @Override
+ public void onCustomAction(SimpleExoPlayer player, String action, Bundle extras) {
+ int mode = player.getRepeatMode();
+ int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
+ if (mode != proposedMode) {
+ player.setRepeatMode(proposedMode);
+ }
+ }
+
+ @Override
+ public PlaybackStateCompat.CustomAction getCustomAction(SimpleExoPlayer player) {
+ CharSequence actionLabel;
+ int iconResourceId;
+ switch (player.getRepeatMode()) {
+ case ExoPlayer.REPEAT_MODE_ONE:
+ actionLabel = repeatOneDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_one;
+ break;
+ case ExoPlayer.REPEAT_MODE_ALL:
+ actionLabel = repeatAllDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_all;
+ break;
+ case ExoPlayer.REPEAT_MODE_OFF:
+ default:
+ actionLabel = repeatOffDescription;
+ iconResourceId = R.drawable.exo_media_action_repeat_off;
+ break;
+ }
+ PlaybackStateCompat.CustomAction.Builder repeatBuilder = new PlaybackStateCompat.CustomAction
+ .Builder(ACTION_REPEAT_MODE, actionLabel, iconResourceId);
+ return repeatBuilder.build();
+ }
+
+}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
new file mode 100644
index 0000000000..97959ccf12
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -0,0 +1,169 @@
+/*
+ * 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.support.annotation.Nullable;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that's based on an
+ * {@link ExoPlayer}'s current {@link Timeline} and maps the timeline of the player to the media
+ * session queue.
+ */
+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;
+ private final int maxQueueSize;
+
+ private long activeQueueItemId;
+
+ /**
+ * Creates a new timeline queue navigator for a given {@link MediaSessionCompat}.
+ *
+ * This is equivalent to calling
+ * {@code #TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}.
+ *
+ * @param mediaSession The {@link MediaSessionCompat}.
+ */
+ public TimelineQueueNavigator(MediaSessionCompat mediaSession) {
+ this(mediaSession, DEFAULT_MAX_QUEUE_SIZE);
+ }
+
+ /**
+ * Creates a new timeline queue navigator for a given {@link MediaSessionCompat} and a maximum
+ * queue size of {@code maxQueueSize}.
+ *
+ * If the actual queue size is larger than {@code maxQueueSize} a floating window of
+ * {@code maxQueueSize} is applied and moved back and forth when the user is navigating within the
+ * queue.
+ *
+ * @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 getSupportedPlaybackActions() {
+ return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+ }
+
+ @Override
+ public void onTimelineChanged(ExoPlayer player) {
+ publishFloatingQueueWindow(player);
+ }
+
+ @Override
+ public void onCurrentWindowIndexChanged(ExoPlayer player) {
+ publishFloatingQueueWindow(player);
+ }
+
+ @Override
+ public final long getActiveQueueItemId(@Nullable ExoPlayer player) {
+ return activeQueueItemId;
+ }
+
+ @Override
+ public final void onSkipToPrevious(ExoPlayer player) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
+ return;
+ }
+ int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(),
+ player.getRepeatMode());
+ 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 final void onSkipToQueueItem(ExoPlayer 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 final void onSkipToNext(ExoPlayer player) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
+ return;
+ }
+ int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(),
+ player.getRepeatMode());
+ if (nextWindowIndex != C.INDEX_UNSET) {
+ player.seekTo(nextWindowIndex, C.TIME_UNSET);
+ }
+ }
+
+ @Override
+ public void onSetShuffleModeEnabled(ExoPlayer player, boolean enabled) {
+ // TODO: Implement this.
+ }
+
+ private void publishFloatingQueueWindow(ExoPlayer 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;
+ }
+
+}
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml
new file mode 100644
index 0000000000..dad37fa1f0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml
new file mode 100644
index 0000000000..132eae0d76
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml
new file mode 100644
index 0000000000..d51010566a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..2824e7847c
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..0b92f583da
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..232aa2b1cd
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..5c91a47519
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..a94abd864f
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..a59a985239
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..97f7e1cc75
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..6a02321702
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..59bac33705
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..2baaedecbf
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..2468f92f9f
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..4e1d53db77
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png
new file mode 100644
index 0000000000..d7207ebc0d
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png
new file mode 100644
index 0000000000..4d6253ead6
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png differ
diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png
new file mode 100644
index 0000000000..d577f4ebcd
Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png differ
diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000000..4ef78cd84f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-af/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Herhaal alles"
+ "Herhaal niks"
+ "Herhaal een"
+
diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..531f605584
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-am/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ሁሉንም ድገም"
+ "ምንም አትድገም"
+ "አንዱን ድገም"
+
diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..0101a746e0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ar/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "تكرار الكل"
+ "عدم التكرار"
+ "تكرار مقطع واحد"
+
diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
new file mode 100644
index 0000000000..34408143fa
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Bütün təkrarlayın"
+ "Təkrar bir"
+ "Heç bir təkrar"
+
diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000000..67a51cf85e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi sve"
+ "Ne ponavljaj nijednu"
+ "Ponovi jednu"
+
diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
new file mode 100644
index 0000000000..2f05607235
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Паўтарыць усё"
+ "Паўтараць ні"
+ "Паўтарыць адзін"
+
diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..16910d640a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bg/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повтаряне на всички"
+ "Без повтаряне"
+ "Повтаряне на един елемент"
+
diff --git a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000000..8872b464c6
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "সবগুলির পুনরাবৃত্তি করুন"
+ "একটিরও পুনরাবৃত্তি করবেন না"
+ "একটির পুনরাবৃত্তি করুন"
+
diff --git a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
new file mode 100644
index 0000000000..d0bf068573
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovite sve"
+ "Ne ponavljaju"
+ "Ponovite jedan"
+
diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..89414d736e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ca/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeteix-ho tot"
+ "No en repeteixis cap"
+ "Repeteix-ne un"
+
diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..784d872570
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-cs/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Opakovat vše"
+ "Neopakovat"
+ "Opakovat jednu položku"
+
diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..2c9784d122
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-da/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Gentag alle"
+ "Gentag ingen"
+ "Gentag en"
+
diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..c11e449665
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-de/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Alle wiederholen"
+ "Keinen Titel wiederholen"
+ "Einen Titel wiederholen"
+
diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..6279af5d64
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-el/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Επανάληψη όλων"
+ "Καμία επανάληψη"
+ "Επανάληψη ενός στοιχείου"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000000..a3fccf8b52
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repeat all"
+ "Repeat none"
+ "Repeat one"
+
diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000000..0fe29d3d5a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "No repetir"
+ "Repetir uno"
+
diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..0fe29d3d5a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-es/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "No repetir"
+ "Repetir uno"
+
diff --git a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000000..1bc3b59706
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Korda kõike"
+ "Ära korda midagi"
+ "Korda ühte"
+
diff --git a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000000..f15f03160f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Errepikatu guztiak"
+ "Ez errepikatu"
+ "Errepikatu bat"
+
diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..e37a08de64
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fa/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "تکرار همه"
+ "تکرار هیچکدام"
+ "یکبار تکرار"
+
diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..c920827976
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Toista kaikki"
+ "Toista ei mitään"
+ "Toista yksi"
+
diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000000..c5191e74a9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tout lire en boucle"
+ "Aucune répétition"
+ "Répéter un élément"
+
diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..1d76358d1f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-fr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tout lire en boucle"
+ "Ne rien lire en boucle"
+ "Lire en boucle un élément"
+
diff --git a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000000..6b65b3e843
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir todo"
+ "Non repetir"
+ "Repetir un"
+
diff --git a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..0eb9cab37e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "બધા પુનરાવર્તન કરો"
+ "કંઈ પુનરાવર્તન કરો"
+ "એક પુનરાવર્તન કરો"
+
diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000000..8ce336d5e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सभी को दोहराएं"
+ "कुछ भी न दोहराएं"
+ "एक दोहराएं"
+
diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..9f995ec15b
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi sve"
+ "Bez ponavljanja"
+ "Ponovi jedno"
+
diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..2335ade72e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hu/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Összes ismétlése"
+ "Nincs ismétlés"
+ "Egy ismétlése"
+
diff --git a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..19a89e6c87
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "կրկնել այն ամենը"
+ "Չկրկնել"
+ "Կրկնել մեկը"
+
diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..093a7f8576
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-in/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulangi Semua"
+ "Jangan Ulangi"
+ "Ulangi Satu"
+
diff --git a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000000..b200abbdb2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Endurtaka allt"
+ "Endurtaka ekkert"
+ "Endurtaka eitt"
+
diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..c0682519f9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-it/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ripeti tutti"
+ "Non ripetere nessuno"
+ "Ripeti uno"
+
diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..5cf23d5a4c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-iw/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "חזור על הכל"
+ "אל תחזור על כלום"
+ "חזור על פריט אחד"
+
diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..6f543fbdee
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ja/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "全曲を繰り返し"
+ "繰り返しなし"
+ "1曲を繰り返し"
+
diff --git a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000000..96656612a7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "გამეორება ყველა"
+ "გაიმეორეთ არცერთი"
+ "გაიმეორეთ ერთი"
+
diff --git a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000000..be4140120d
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Барлығын қайталау"
+ "Ешқайсысын қайталамау"
+ "Біреуін қайталау"
+
diff --git a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000000..dd4b734e30
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ធ្វើម្ដងទៀតទាំងអស់"
+ "មិនធ្វើឡើងវិញ"
+ "ធ្វើឡើងវិញម្ដង"
+
diff --git a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000000..3d79aca9e2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ"
+ "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ"
+ "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ"
+
diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..d269937771
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ko/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "전체 반복"
+ "반복 안함"
+ "한 항목 반복"
+
diff --git a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000000..a8978ecc61
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Баарын кайталоо"
+ "Эч бирин кайталабоо"
+ "Бирөөнү кайталоо"
+
diff --git a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000000..950a9ba097
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ຫຼິ້ນຊ້ຳທັງໝົດ"
+ "ບໍ່ຫຼິ້ນຊ້ຳ"
+ "ຫຼິ້ນຊ້ຳ"
+
diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..ae8f1cf8c3
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lt/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Kartoti viską"
+ "Nekartoti nieko"
+ "Kartoti vieną"
+
diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000000..a69f6a0ad5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lv/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Atkārtot visu"
+ "Neatkārtot nevienu"
+ "Atkārtot vienu"
+
diff --git a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000000..ddf2a60c20
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повтори ги сите"
+ "Не повторувај ниту една"
+ "Повтори една"
+
diff --git a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000000..6f869e2931
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "എല്ലാം ആവർത്തിക്കുക"
+ "ഒന്നും ആവർത്തിക്കരുത്"
+ "ഒന്ന് ആവർത്തിക്കുക"
+
diff --git a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000000..8d3074b91a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Бүгдийг давтах"
+ "Алийг нь ч давтахгүй"
+ "Нэгийг давтах"
+
diff --git a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000000..6e4bfccc16
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सर्व पुनरावृत्ती करा"
+ "काहीही पुनरावृत्ती करू नका"
+ "एक पुनरावृत्ती करा"
+
diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000000..829542b668
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulang semua"
+ "Tiada ulangan"
+ "Ulangan"
+
diff --git a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000000..aeb1375ebf
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "အားလုံး ထပ်တလဲလဲဖွင့်ရန်"
+ "ထပ်တလဲလဲမဖွင့်ရန်"
+ "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်"
+
diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000000..10f334b226
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-nb/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Gjenta alle"
+ "Ikke gjenta noen"
+ "Gjenta én"
+
diff --git a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..6d81ce5684
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "सबै दोहोर्याउनुहोस्"
+ "कुनै पनि नदोहोर्याउनुहोस्"
+ "एउटा दोहोर्याउनुहोस्"
+
diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..55997be098
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-nl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Alles herhalen"
+ "Niet herhalen"
+ "Eén herhalen"
+
diff --git a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8eee0bee16
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "ਸਭ ਨੂੰ ਦੁਹਰਾਓ"
+ "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ"
+ "ਇੱਕ ਦੁਹਰਾਓ"
+
diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6a52d58b63
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Powtórz wszystkie"
+ "Nie powtarzaj"
+ "Powtórz jeden"
+
diff --git a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..efb8fc433f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir um"
+
diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..efb8fc433f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir um"
+
diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000000..aadebbb3b0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pt/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetir tudo"
+ "Não repetir"
+ "Repetir uma"
+
diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..f6aee447e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ro/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Repetați toate"
+ "Repetați niciuna"
+ "Repetați unul"
+
diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..575ad9f930
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ru/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повторять все"
+ "Не повторять"
+ "Повторять один элемент"
+
diff --git a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000000..8e172ac268
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "සියලු නැවත"
+ "කිසිවක් නැවත"
+ "නැවත නැවත එක්"
+
diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..5d092003e5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sk/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Opakovať všetko"
+ "Neopakovať"
+ "Opakovať jednu položku"
+
diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..ecac3800c8
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ponovi vse"
+ "Ne ponovi"
+ "Ponovi eno"
+
diff --git a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
new file mode 100644
index 0000000000..6da24cc4c7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Përsërit të gjithë"
+ "Përsëritni asnjë"
+ "Përsëritni një"
+
diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..881cb2703b
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sr/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000000..3a7bb630aa
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sv/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Upprepa alla"
+ "Upprepa inga"
+ "Upprepa en"
+
diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000000..726012ab88
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sw/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Rudia zote"
+ "Usirudie Yoyote"
+ "Rudia Moja"
+
diff --git a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000000..9364bc0be2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "அனைத்தையும் மீண்டும் இயக்கு"
+ "எதையும் மீண்டும் இயக்காதே"
+ "ஒன்றை மட்டும் மீண்டும் இயக்கு"
+
diff --git a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000000..b7ee7345d5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "అన్నీ పునరావృతం చేయి"
+ "ఏదీ పునరావృతం చేయవద్దు"
+ "ఒకదాన్ని పునరావృతం చేయి"
+
diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..af502b3a4c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-th/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "เล่นซ้ำทั้งหมด"
+ "ไม่เล่นซ้ำ"
+ "เล่นซ้ำรายการเดียว"
+
diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..239972a4c7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-tl/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Ulitin Lahat"
+ "Walang Uulitin"
+ "Ulitin ang Isa"
+
diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..89a98b1ed9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-tr/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Tümünü Tekrarla"
+ "Hiçbirini Tekrarlama"
+ "Birini Tekrarla"
+
diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..4e1d25eb8a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-uk/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Повторити все"
+ "Не повторювати"
+ "Повторити один елемент"
+
diff --git a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000000..ab2631a4ec
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "سبھی کو دہرائیں"
+ "کسی کو نہ دہرائیں"
+ "ایک کو دہرائیں"
+
diff --git a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000000..c32d00af8e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Barchasini takrorlash"
+ "Takrorlamaslik"
+ "Bir marta takrorlash"
+
diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..dabc9e05d5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-vi/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Lặp lại tất cả"
+ "Không lặp lại"
+ "Lặp lại một mục"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..beb3403cb9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重复播放全部"
+ "不重复播放"
+ "重复播放单个视频"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000000..775cd6441c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重複播放所有媒體項目"
+ "不重複播放任何媒體項目"
+ "重複播放一個媒體項目"
+
diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..d3789f4145
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "重複播放所有媒體項目"
+ "不重複播放"
+ "重複播放單一媒體項目"
+
diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000000..789b6fecb4
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-zu/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ "Phinda konke"
+ "Ungaphindi lutho"
+ "Phida okukodwa"
+
diff --git a/extensions/mediasession/src/main/res/values/strings.xml b/extensions/mediasession/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..72a67ff01c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Repeat none
+ Repeat one
+ Repeat all
+
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
new file mode 100644
index 0000000000..07850269f9
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
@@ -0,0 +1,93 @@
+/*
+ * 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.util;
+
+import android.support.annotation.IntDef;
+
+import com.google.android.exoplayer2.ExoPlayer;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Util class for repeat mode handling.
+ */
+public final class RepeatModeUtil {
+
+ /**
+ * Set of repeat toggle modes. Can be combined using bit-wise operations.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE,
+ REPEAT_TOGGLE_MODE_ALL})
+ public @interface RepeatToggleModes {}
+ /**
+ * All repeat mode buttons disabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_NONE = 0;
+ /**
+ * "Repeat One" button enabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_ONE = 1;
+ /**
+ * "Repeat All" button enabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_ALL = 2;
+
+ private RepeatModeUtil() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}.
+ *
+ * @param currentMode The current repeat mode.
+ * @param enabledModes Bitmask of enabled modes.
+ * @return The next repeat mode.
+ */
+ public static @ExoPlayer.RepeatMode int getNextRepeatMode(
+ @ExoPlayer.RepeatMode int currentMode, int enabledModes) {
+ for (int offset = 1; offset <= 2; offset++) {
+ @ExoPlayer.RepeatMode int proposedMode = (currentMode + offset) % 3;
+ if (isRepeatModeEnabled(proposedMode, enabledModes)) {
+ return proposedMode;
+ }
+ }
+ return currentMode;
+ }
+
+ /**
+ * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}.
+ *
+ * @param repeatMode The mode to check.
+ * @param enabledModes The bitmask representing the enabled modes.
+ * @return {@code true} if enabled.
+ */
+ public static boolean isRepeatModeEnabled(@ExoPlayer.RepeatMode int repeatMode,
+ int enabledModes) {
+ switch (repeatMode) {
+ case ExoPlayer.REPEAT_MODE_OFF:
+ return true;
+ case ExoPlayer.REPEAT_MODE_ONE:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0;
+ case ExoPlayer.REPEAT_MODE_ALL:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0;
+ default:
+ return false;
+ }
+ }
+
+}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
index ca1a967434..6a4f258dd2 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
@@ -22,7 +22,6 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
-import android.support.annotation.IntDef;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -39,9 +38,8 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Locale;
@@ -82,7 +80,7 @@ import java.util.Locale;
* {@code all}, or {@code one|all}.
*
* - Corresponding method: {@link #setRepeatToggleModes(int)}
- * - Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES}
+ * - Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
*
*
* {@code controller_layout_id} - Specifies the id of the layout to be inflated. See
@@ -249,30 +247,11 @@ public class PlaybackControlView extends FrameLayout {
};
- /**
- * Set of repeat toggle modes. Can be combined using bit-wise operations.
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE,
- REPEAT_TOGGLE_MODE_ALL})
- public @interface RepeatToggleModes {}
- /**
- * All repeat mode buttons disabled.
- */
- public static final int REPEAT_TOGGLE_MODE_NONE = 0;
- /**
- * "Repeat One" button enabled.
- */
- public static final int REPEAT_TOGGLE_MODE_ONE = 1;
- /**
- * "Repeat All" button enabled.
- */
- public static final int REPEAT_TOGGLE_MODE_ALL = 2;
-
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
- public static final @RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = REPEAT_TOGGLE_MODE_NONE;
+ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES
+ = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;
/**
* The maximum number of windows that can be shown in a multi-window time bar.
@@ -315,7 +294,7 @@ public class PlaybackControlView extends FrameLayout {
private int rewindMs;
private int fastForwardMs;
private int showTimeoutMs;
- private @RepeatToggleModes int repeatToggleModes;
+ private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
private long hideAtMs;
private long[] adGroupTimesMs;
private boolean[] playedAdGroups;
@@ -424,8 +403,8 @@ public class PlaybackControlView extends FrameLayout {
}
@SuppressWarnings("ResourceType")
- private static @RepeatToggleModes int getRepeatToggleModes(TypedArray a,
- @RepeatToggleModes int repeatToggleModes) {
+ private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(TypedArray a,
+ @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes);
}
@@ -535,28 +514,28 @@ public class PlaybackControlView extends FrameLayout {
/**
* Returns which repeat toggle modes are enabled.
*
- * @return The currently enabled {@link RepeatToggleModes}.
+ * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.
*/
- public @RepeatToggleModes int getRepeatToggleModes() {
+ public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() {
return repeatToggleModes;
}
/**
* Sets which repeat toggle modes are enabled.
*
- * @param repeatToggleModes A set of {@link RepeatToggleModes}.
+ * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
*/
- public void setRepeatToggleModes(@RepeatToggleModes int repeatToggleModes) {
+ public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.repeatToggleModes = repeatToggleModes;
if (player != null) {
@ExoPlayer.RepeatMode int currentMode = player.getRepeatMode();
- if (repeatToggleModes == REPEAT_TOGGLE_MODE_NONE
+ if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
&& currentMode != ExoPlayer.REPEAT_MODE_OFF) {
controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_OFF);
- } else if (repeatToggleModes == REPEAT_TOGGLE_MODE_ONE
+ } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
&& currentMode == ExoPlayer.REPEAT_MODE_ALL) {
controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_ONE);
- } else if (repeatToggleModes == REPEAT_TOGGLE_MODE_ALL
+ } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
&& currentMode == ExoPlayer.REPEAT_MODE_ONE) {
controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_ALL);
}
@@ -674,7 +653,7 @@ public class PlaybackControlView extends FrameLayout {
if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
return;
}
- if (repeatToggleModes == REPEAT_TOGGLE_MODE_NONE) {
+ if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
repeatToggleButton.setVisibility(View.GONE);
return;
}
@@ -859,30 +838,6 @@ public class PlaybackControlView extends FrameLayout {
}
}
- private @ExoPlayer.RepeatMode int getNextRepeatMode() {
- @ExoPlayer.RepeatMode int currentMode = player.getRepeatMode();
- for (int offset = 1; offset <= 2; offset++) {
- @ExoPlayer.RepeatMode int proposedMode = (currentMode + offset) % 3;
- if (isRepeatModeEnabled(proposedMode)) {
- return proposedMode;
- }
- }
- return currentMode;
- }
-
- private boolean isRepeatModeEnabled(@ExoPlayer.RepeatMode int repeatMode) {
- switch (repeatMode) {
- case ExoPlayer.REPEAT_MODE_OFF:
- return true;
- case ExoPlayer.REPEAT_MODE_ONE:
- return (repeatToggleModes & REPEAT_TOGGLE_MODE_ONE) != 0;
- case ExoPlayer.REPEAT_MODE_ALL:
- return (repeatToggleModes & REPEAT_TOGGLE_MODE_ALL) != 0;
- default:
- return false;
- }
- }
-
private void rewind() {
if (rewindMs <= 0) {
return;
@@ -1126,7 +1081,8 @@ public class PlaybackControlView extends FrameLayout {
} else if (pauseButton == view) {
controlDispatcher.dispatchSetPlayWhenReady(player, false);
} else if (repeatToggleButton == view) {
- controlDispatcher.dispatchSetRepeatMode(player, getNextRepeatMode());
+ controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode(
+ player.getRepeatMode(), repeatToggleModes));
}
}
hideAfterTimeout();
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
index 7efa2c87ba..b3c79b9fdc 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
@@ -49,6 +49,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.PlaybackControlView.ControlDispatcher;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
@@ -637,9 +638,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
/**
* Sets which repeat toggle modes are enabled.
*
- * @param repeatToggleModes A set of {@link PlaybackControlView.RepeatToggleModes}.
+ * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
*/
- public void setRepeatToggleModes(@PlaybackControlView.RepeatToggleModes int repeatToggleModes) {
+ public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
Assertions.checkState(controller != null);
controller.setRepeatToggleModes(repeatToggleModes);
}