Merge "Add get flip state and rotation degree method" into oc-support-26.1-dev
diff --git a/api/27.0.0-SNAPSHOT.txt b/api/27.0.0-SNAPSHOT.txt
index d4fe232..4e7c0ef 100644
--- a/api/27.0.0-SNAPSHOT.txt
+++ b/api/27.0.0-SNAPSHOT.txt
@@ -3604,6 +3604,16 @@
package android.support.v17.leanback.media {
+ public class MediaControllerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
+ ctor public MediaControllerAdapter(android.support.v4.media.session.MediaControllerCompat);
+ method public android.graphics.drawable.Drawable getMediaArt(android.content.Context);
+ method public android.support.v4.media.session.MediaControllerCompat getMediaController();
+ method public java.lang.CharSequence getMediaSubtitle();
+ method public java.lang.CharSequence getMediaTitle();
+ method public void pause();
+ method public void play();
+ }
+
public abstract class MediaControllerGlue extends android.support.v17.leanback.media.PlaybackControlGlue {
ctor public MediaControllerGlue(android.content.Context, int[], int[]);
method public void attachToMediaController(android.support.v4.media.session.MediaControllerCompat);
@@ -3637,7 +3647,6 @@
ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T);
method public int[] getFastForwardSpeeds();
method public int[] getRewindSpeeds();
- method public long getSupportedActions();
method public void onActionClicked(android.support.v17.leanback.widget.Action);
method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
method public boolean onKey(android.view.View, int, android.view.KeyEvent);
@@ -3668,6 +3677,7 @@
method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
method public final T getPlayerAdapter();
method public java.lang.CharSequence getSubtitle();
+ method public long getSupportedActions();
method public java.lang.CharSequence getTitle();
method public boolean isControlsOverlayAutoHideEnabled();
method public final boolean isPlaying();
@@ -3678,6 +3688,7 @@
method protected abstract android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method protected void onMetadataChanged();
method protected void onPlayCompleted();
method protected void onPlayStateChanged();
method protected void onPreparedStateChanged();
@@ -3688,6 +3699,15 @@
method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
method public void setSubtitle(java.lang.CharSequence);
method public void setTitle(java.lang.CharSequence);
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REPEAT = 512; // 0x200
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SHUFFLE = 1024; // 0x400
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
}
public abstract class PlaybackControlGlue extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
@@ -3820,19 +3840,26 @@
public abstract class PlayerAdapter {
ctor public PlayerAdapter();
+ method public void fastForward();
method public long getBufferedPosition();
method public final android.support.v17.leanback.media.PlayerAdapter.Callback getCallback();
method public long getCurrentPosition();
method public long getDuration();
+ method public long getSupportedActions();
method public boolean isPlaying();
method public boolean isPrepared();
+ method public void next();
method public void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
method public void onDetachedFromHost();
method public abstract void pause();
method public abstract void play();
+ method public void previous();
+ method public void rewind();
method public void seekTo(long);
method public final void setCallback(android.support.v17.leanback.media.PlayerAdapter.Callback);
method public void setProgressUpdatingEnabled(boolean);
+ method public void setRepeatAction(int);
+ method public void setShuffleAction(int);
}
public static class PlayerAdapter.Callback {
@@ -3842,6 +3869,7 @@
method public void onCurrentPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onDurationChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onError(android.support.v17.leanback.media.PlayerAdapter, int, java.lang.String);
+ method public void onMetadataChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onPlayCompleted(android.support.v17.leanback.media.PlayerAdapter);
method public void onPlayStateChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onPreparedStateChanged(android.support.v17.leanback.media.PlayerAdapter);
@@ -10518,10 +10546,6 @@
ctor public deprecated NotificationCompat.Builder(android.content.Context);
}
- public static deprecated class NotificationCompat.DecoratedCustomViewStyle extends android.support.v4.app.NotificationCompat.DecoratedCustomViewStyle {
- ctor public deprecated NotificationCompat.DecoratedCustomViewStyle();
- }
-
public static deprecated class NotificationCompat.DecoratedMediaCustomViewStyle extends android.support.v4.media.app.NotificationCompat.DecoratedMediaCustomViewStyle {
ctor public deprecated NotificationCompat.DecoratedMediaCustomViewStyle();
}
diff --git a/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java b/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java
index 16a7607..00e102f 100644
--- a/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java
+++ b/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java
@@ -300,18 +300,19 @@
private class IteratorWithAdditions implements Iterator<Map.Entry<K, V>>, SupportRemove<K, V> {
private Entry<K, V> mCurrent;
- private boolean mFirstStep = true;
+ private boolean mBeforeStart = true;
@Override
public void supportRemove(@NonNull Entry<K, V> entry) {
if (entry == mCurrent) {
mCurrent = mCurrent.mPrevious;
+ mBeforeStart = mCurrent == null;
}
}
@Override
public boolean hasNext() {
- if (mFirstStep) {
+ if (mBeforeStart) {
return mStart != null;
}
return mCurrent != null && mCurrent.mNext != null;
@@ -319,8 +320,8 @@
@Override
public Map.Entry<K, V> next() {
- if (mFirstStep) {
- mFirstStep = false;
+ if (mBeforeStart) {
+ mBeforeStart = false;
mCurrent = mStart;
} else {
mCurrent = mCurrent != null ? mCurrent.mNext : null;
diff --git a/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java b/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java
index 4b25e34..d879543 100644
--- a/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java
+++ b/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java
@@ -200,6 +200,20 @@
}
@Test
+ public void testRemoveDuringIteration4() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
+ int[] expected = new int[]{1, 2};
+ int index = 0;
+ for (Entry<Integer, Boolean> entry : map) {
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.remove(1);
+ }
+ }
+ assertThat(index, is(2));
+ }
+
+ @Test
public void testAdditionDuringIteration() {
SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
int[] expected = new int[]{1, 2, 3, 4};
@@ -317,6 +331,22 @@
}
@Test
+ public void testIteratorWithAddition5() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
+ int[] expected = new int[]{1, 2};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.remove(1);
+ }
+ }
+ assertThat(index, is(2));
+ }
+
+ @Test
public void testDescendingIteration() {
SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
int[] expected = new int[]{4, 3, 2, 1};
diff --git a/droiddoc.mk b/droiddoc.mk
index 0913f5f..a4efc51 100644
--- a/droiddoc.mk
+++ b/droiddoc.mk
@@ -37,6 +37,4 @@
-since $(SUPPORT_PATH)/api/25.2.0.txt 25.2.0 \
-since $(SUPPORT_PATH)/api/25.3.0.txt 25.3.0 \
-since $(SUPPORT_PATH)/api/25.4.0.txt 25.4.0 \
- -since $(SUPPORT_PATH)/api/26.0.0-alpha1.txt 26.0.0-alpha1 \
- -since $(SUPPORT_PATH)/api/26.0.0-beta1.txt 26.0.0-beta1 \
- -since $(SUPPORT_PATH)/api/26.0.0-beta2.txt 26.0.0-beta2
+ -since $(SUPPORT_PATH)/api/26.0.0.txt 26.0.0
diff --git a/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java b/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
index f401e1c..1802d94 100644
--- a/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
+++ b/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
@@ -368,6 +368,22 @@
}
@Test
+ public void testRemoveDuringSetValue() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ final Observer observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(String o) {
+ mLiveData.removeObserver(this);
+ }
+ });
+ Observer<String> observer2 = (Observer<String>) mock(Observer.class);
+ mLiveData.observeForever(observer1);
+ mLiveData.observe(mOwner, observer2);
+ mLiveData.setValue("gt");
+ verify(observer2).onChanged("gt");
+ }
+
+ @Test
public void testDataChangeDuringStateChange() {
mRegistry.handleLifecycleEvent(ON_START);
mRegistry.addObserver(new LifecycleObserver() {
diff --git a/samples/SupportLeanbackDemos/AndroidManifest.xml b/samples/SupportLeanbackDemos/AndroidManifest.xml
index 010c297..7b8b946 100644
--- a/samples/SupportLeanbackDemos/AndroidManifest.xml
+++ b/samples/SupportLeanbackDemos/AndroidManifest.xml
@@ -4,6 +4,14 @@
android:versionCode="1"
android:versionName="1.0">
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="true"/>
+
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false"/>
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -192,5 +200,11 @@
<activity android:name=".VideoActivityWithDetailedCard"
android:exported="true" />
+ <activity
+ android:name=".MusicExampleActivity"
+ android:exported="true"/>
+
+ <service android:exported="false" android:name=".MediaSessionService"/>
+
</application>
</manifest>
diff --git a/samples/SupportLeanbackDemos/build.gradle b/samples/SupportLeanbackDemos/build.gradle
index 014cd16..227ff51 100644
--- a/samples/SupportLeanbackDemos/build.gradle
+++ b/samples/SupportLeanbackDemos/build.gradle
@@ -3,6 +3,7 @@
dependencies {
implementation project(':leanback-v17')
implementation project(':preference-leanback-v17')
+ implementation 'com.google.code.gson:gson:2.6.2'
}
android {
diff --git a/samples/SupportLeanbackDemos/res/drawable/google_android.png b/samples/SupportLeanbackDemos/res/drawable/google_android.png
new file mode 100644
index 0000000..d3e90f8
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/drawable/google_android.png
Binary files differ
diff --git a/samples/SupportLeanbackDemos/res/drawable/google_logo.png b/samples/SupportLeanbackDemos/res/drawable/google_logo.png
new file mode 100644
index 0000000..63f399d
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/drawable/google_logo.png
Binary files differ
diff --git a/samples/SupportLeanbackDemos/res/drawable/google_photo.jpeg b/samples/SupportLeanbackDemos/res/drawable/google_photo.jpeg
new file mode 100644
index 0000000..5d50f2d
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/drawable/google_photo.jpeg
Binary files differ
diff --git a/samples/SupportLeanbackDemos/res/layout/activity_music_example.xml b/samples/SupportLeanbackDemos/res/layout/activity_music_example.xml
new file mode 100644
index 0000000..a6b4d4f
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/layout/activity_music_example.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <!--
+ Copyright (C) 2015 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.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <fragment
+ android:id="@+id/musicFragment"
+ android:name="com.example.android.leanback.MusicPlayerFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"></fragment>
+</RelativeLayout>
\ No newline at end of file
diff --git a/samples/SupportLeanbackDemos/res/raw/media.json b/samples/SupportLeanbackDemos/res/raw/media.json
new file mode 100644
index 0000000..5545f15
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/raw/media.json
@@ -0,0 +1,30 @@
+[
+ {
+ "title": "Title0",
+ "description": "Description0",
+ "art": "google_map",
+ "file": "media0",
+ "duration":"2:15"
+ },
+ {
+ "title": "Title1",
+ "description": "Description1",
+ "art": "google_photo",
+ "file": "media1",
+ "duration":"3:32"
+ },
+ {
+ "title": "Title2",
+ "description": "Description2",
+ "art": "google_logo",
+ "file": "media0",
+ "duration":"2:15"
+ },
+ {
+ "title": "Title3",
+ "description": "Description3",
+ "art": "google_android",
+ "file": "media1",
+ "duration":"3:32"
+ }
+]
\ No newline at end of file
diff --git a/samples/SupportLeanbackDemos/res/raw/media0.mp3 b/samples/SupportLeanbackDemos/res/raw/media0.mp3
new file mode 100644
index 0000000..8316222
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/raw/media0.mp3
Binary files differ
diff --git a/samples/SupportLeanbackDemos/res/raw/media1.mp3 b/samples/SupportLeanbackDemos/res/raw/media1.mp3
new file mode 100644
index 0000000..c5a1d25
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/raw/media1.mp3
Binary files differ
diff --git a/samples/SupportLeanbackDemos/res/values/strings.xml b/samples/SupportLeanbackDemos/res/values/strings.xml
index 4e4a398..d3314a4 100644
--- a/samples/SupportLeanbackDemos/res/values/strings.xml
+++ b/samples/SupportLeanbackDemos/res/values/strings.xml
@@ -98,4 +98,7 @@
<string name="guidedstep_fourth_title">Fourth</string>
<string name="guidedstep_fourth_description">Fourth step of guided sequence</string>
<string name="guidedstep_fourth_breadcrumb">Guided Steps</string>
+
+ <string name="music">Music player</string>
+ <string name="music_description">Music player using control glue</string>
</resources>
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
index 4293dbb..116003f 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
@@ -146,6 +146,10 @@
addAction(actions, VideoActivityWithDetailedCard.class,
R.string.video_play_with_detail_card,
R.string.video_play_with_detail_card_description);
+
+ addAction(actions, MusicExampleActivity.class,
+ R.string.music,
+ R.string.music_description);
}
private void addAction(List<GuidedAction> actions, Class cls, int titleRes, int descRes) {
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MediaSessionService.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MediaSessionService.java
new file mode 100644
index 0000000..2c31c2b
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MediaSessionService.java
@@ -0,0 +1,1020 @@
+/*
+ * 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.example.android.leanback;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * The service to play music. It also contains the media session.
+ */
+public class MediaSessionService extends Service {
+
+
+ public static final String CANNOT_SET_DATA_SOURCE = "Cannot set data source";
+ private static final float NORMAL_SPEED = 1.0f;
+
+ /**
+ * When media player is prepared, our service can send notification to UI side through this
+ * callback. So UI will have chance to prepare/ pre-processing the UI status.
+ */
+ interface MediaPlayerListener {
+ void onPrepared();
+ }
+
+ /**
+ * This LocalBinder class contains the getService() method which will return the service object.
+ */
+ public class LocalBinder extends Binder {
+ MediaSessionService getService() {
+ return MediaSessionService.this;
+ }
+ }
+
+ /**
+ * Constant used in this class.
+ */
+ private static final String MUSIC_PLAYER_SESSION_TOKEN = "MusicPlayer Session token";
+ private static final int MEDIA_ACTION_NO_REPEAT = 0;
+ private static final int MEDIA_ACTION_REPEAT_ONE = 1;
+ private static final int MEDIA_ACTION_REPEAT_ALL = 2;
+ public static final String MEDIA_PLAYER_ERROR_MESSAGE = "Media player error message";
+ public static final String PLAYER_NOT_INITIALIZED = "Media player not initialized";
+ public static final String PLAYER_IS_PLAYING = "Media player is playing";
+ public static final String PLAYER_SET_DATA_SOURCE_ERROR =
+ "Media player set new data source error";
+ private static final boolean DEBUG = false;
+ private static final String TAG = "MusicPlaybackService";
+ private static final int FOCUS_CHANGE = 0;
+
+ // This handler can control media player through audio's status.
+ private class MediaPlayerAudioHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case FOCUS_CHANGE:
+ switch (msg.arg1) {
+ // pause media item when audio focus is lost
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (isPlaying()) {
+ audioFocusLossHandler();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (isPlaying()) {
+ audioLossFocusTransientHandler();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (isPlaying()) {
+ audioLossFocusTransientCanDuckHanlder();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (!isPlaying()) {
+ audioFocusGainHandler();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // The callbacks' collection which can be notified by this service.
+ private List<MediaPlayerListener> mCallbacks = new ArrayList<>();
+
+ // audio manager obtained from system to gain audio focus
+ private AudioManager mAudioManager;
+
+ // record user defined repeat mode.
+ private int mRepeatState = MEDIA_ACTION_NO_REPEAT;
+
+ // record user defined shuffle mode.
+ private int mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+
+ private MediaPlayer mPlayer;
+ private MediaSessionCompat mMediaSession;
+
+ // set -1 as invalid media item for playing.
+ private int mCurrentIndex = -1;
+ // media item in media playlist.
+ private MusicItem mCurrentMediaItem;
+ // media player's current progress.
+ private int mCurrentPosition;
+ // Buffered Position which will be updated inside of OnBufferingUpdateListener
+ private long mBufferedProgress;
+ List<MusicItem> mMediaItemList = new ArrayList<>();
+ private boolean mInitialized;
+
+ // fast forward/ rewind speed factors and indexes
+ private float[] mFastForwardSpeedFactors;
+ private float[] mRewindSpeedFactors;
+ private int mFastForwardSpeedFactorIndex = 0;
+ private int mRewindSpeedFactorIndex = 0;
+
+ // Flags to indicate if current state is fast forwarding/ rewinding.
+ private boolean mIsFastForwarding;
+ private boolean mIsRewinding;
+
+ // handle audio related event.
+ private Handler mMediaPlayerHandler = new MediaPlayerAudioHandler();
+
+ // The volume we set the media player to when we lose audio focus, but are
+ // allowed to reduce the volume and continue playing.
+ private static final float REDUCED_VOLUME = 0.1f;
+ // The volume we set the media player when we have audio focus.
+ private static final float FULL_VOLUME = 1.0f;
+
+ // Record position when current rewind action begins.
+ private long mRewindStartPosition;
+ // Record the time stamp when current rewind action is ended.
+ private long mRewindEndTime;
+ // Record the time stamp when current rewind action is started.
+ private long mRewindStartTime;
+ // Flag to represent the beginning of rewind operation.
+ private boolean mIsRewindBegin;
+
+ // A runnable object which will delay the execution of mPlayer.stop()
+ private Runnable mDelayedStopRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mPlayer.stop();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_STOPPED).build());
+ }
+ };
+
+ // Listener for audio focus.
+ private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new
+ AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ if (DEBUG) {
+ Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
+ }
+ mMediaPlayerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget();
+ }
+ };
+
+ private final IBinder mBinder = new LocalBinder();
+
+ /**
+ * The public API to gain media session instance from service.
+ *
+ * @return Media Session Instance.
+ */
+ public MediaSessionCompat getMediaSession() {
+ return mMediaSession;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // This service can be created for multiple times, the objects will only be created when
+ // it is null
+ if (mMediaSession == null) {
+ mMediaSession = new MediaSessionCompat(this, MUSIC_PLAYER_SESSION_TOKEN);
+ mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mMediaSession.setCallback(new MediaSessionCallback());
+ }
+
+ if (mAudioManager == null) {
+ // Create audio manager through system service
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ // initialize the player (including activate media session, request audio focus and
+ // set up the listener to listen to player's state)
+ initializePlayer();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ stopForeground(true);
+ mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener);
+ mMediaPlayerHandler.removeCallbacksAndMessages(null);
+ if (mPlayer != null) {
+ // stop and release the media player since it's no longer in use
+ mPlayer.reset();
+ mPlayer.release();
+ mPlayer = null;
+ }
+ if (mMediaSession != null) {
+ mMediaSession.release();
+ mMediaSession = null;
+ }
+ }
+
+ /**
+ * After binding to this service, other component can set Media Item List and prepare
+ * the first item in the list through this function.
+ *
+ * @param mediaItemList A list of media item to play.
+ * @param isQueue When this parameter is true, that meas new items should be appended to
+ * original media item list.
+ * If this parameter is false, the original playlist will be cleared and
+ * replaced with a new media item list.
+ */
+ public void setMediaList(List<MusicItem> mediaItemList, boolean isQueue) {
+ if (!isQueue) {
+ mMediaItemList.clear();
+ }
+ mMediaItemList.addAll(mediaItemList);
+
+ /**
+ * Points to the first media item in play list.
+ */
+ mCurrentIndex = 0;
+ mCurrentMediaItem = mMediaItemList.get(0);
+
+ try {
+ mPlayer.setDataSource(this.getApplicationContext(),
+ mCurrentMediaItem.getMediaSourceUri(getApplicationContext()));
+ // Prepare the player asynchronously, use onPrepared listener as signal.
+ mPlayer.prepareAsync();
+ } catch (IOException e) {
+ PlaybackStateCompat.Builder ret = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_ERROR);
+ ret.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR,
+ PLAYER_SET_DATA_SOURCE_ERROR);
+ }
+ }
+
+ /**
+ * Set Fast Forward Speeds for this media session service.
+ *
+ * @param fastForwardSpeeds The array contains all fast forward speeds.
+ */
+ public void setFastForwardSpeedFactors(int[] fastForwardSpeeds) {
+ mFastForwardSpeedFactors = new float[fastForwardSpeeds.length + 1];
+
+ // Put normal speed factor at the beginning of the array
+ mFastForwardSpeedFactors[0] = 1.0f;
+
+ for (int index = 1; index < mFastForwardSpeedFactors.length; ++index) {
+ mFastForwardSpeedFactors[index] = fastForwardSpeeds[index - 1];
+ }
+ }
+
+ /**
+ * Set Rewind Speeds for this media session service.
+ *
+ * @param rewindSpeeds The array contains all rewind speeds.
+ */
+ public void setRewindSpeedFactors(int[] rewindSpeeds) {
+ mRewindSpeedFactors = new float[rewindSpeeds.length];
+ for (int index = 0; index < mRewindSpeedFactors.length; ++index) {
+ mRewindSpeedFactors[index] = -rewindSpeeds[index];
+ }
+ }
+
+ /**
+ * Prepare the first item in the list. And setup the listener for media player.
+ */
+ private void initializePlayer() {
+ // This service can be created for multiple times, the objects will only be created when
+ // it is null
+ if (mPlayer != null) {
+ return;
+ }
+ mPlayer = new MediaPlayer();
+
+ // Set playback state to none to create a valid playback state. So controls row can get
+ // information about the supported actions.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_NONE).build());
+ // Activate media session
+ if (!mMediaSession.isActive()) {
+ mMediaSession.setActive(true);
+ }
+
+ // Set up listener and audio stream type for underlying music player.
+ mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+
+ // set up listener when the player is prepared.
+ mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ mInitialized = true;
+ // Every time when the player is prepared (when new data source is set),
+ // all listeners will be notified to toggle the UI to "pause" status.
+ notifyUiWhenPlayerIsPrepared();
+
+ // When media player is prepared, the callback functions will be executed to update
+ // the meta data and playback state.
+ onMediaSessionMetaDataChanged();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ }
+ });
+
+ // set up listener for player's error.
+ mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ if (DEBUG) {
+ PlaybackStateCompat.Builder builder = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_ERROR);
+ builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR,
+ MEDIA_PLAYER_ERROR_MESSAGE);
+ mMediaSession.setPlaybackState(builder.build());
+ }
+ return true;
+ }
+ });
+
+ // set up listener to respond the event when current music item is finished
+ mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+
+ /**
+ * Expected Interaction Behavior:
+ * 1. If current media item's playing speed not equal to normal speed.
+ *
+ * A. MEDIA_ACTION_REPEAT_ALL
+ * a. If current media item is the last one. The first music item in the list will
+ * be prepared, but it won't play until user press play button.
+ *
+ * When user press the play button, the speed will be reset to normal (1.0f)
+ * no matter what the previous media item's playing speed is.
+ *
+ * b. If current media item isn't the last one, next media item will be prepared,
+ * but it won't play.
+ *
+ * When user press the play button, the speed will be reset to normal (1.0f)
+ * no matter what the previous media item's playing speed is.
+ *
+ * B. MEDIA_ACTION_REPEAT_ONE
+ * Different with previous scenario, current item will go back to the start point
+ * again and play automatically. (The reason to enable auto play here is for
+ * testing purpose and to make sure our designed API is flexible enough to support
+ * different situations.)
+ *
+ * No matter what the previous media item's playing speed is, in this situation
+ * current media item will be replayed in normal speed.
+ *
+ * C. MEDIA_ACTION_REPEAT_NONE
+ * a. If current media is the last one. The service will be closed, no music item
+ * will be prepared to play. From the UI perspective, the progress bar will not
+ * be reset to the starting point.
+ *
+ * b. If current media item isn't the last one, next media item will be prepared,
+ * but it won't play.
+ *
+ * When user press the play button, the speed will be reset to normal (1.0f)
+ * no matter what the previous media item's playing speed is.
+ *
+ * @param mp Object of MediaPlayer。
+ */
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+
+ // When current media item finishes playing, always reset rewind/ fastforward state
+ mFastForwardSpeedFactorIndex = 0;
+ mRewindSpeedFactorIndex = 0;
+ // Set player's playback speed back to normal
+ mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed(
+ mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]));
+ // Pause the player, and update the status accordingly.
+ mPlayer.pause();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+
+ if (mRepeatState == MEDIA_ACTION_REPEAT_ALL
+ && mCurrentIndex == mMediaItemList.size() - 1) {
+ // if the repeat mode is enabled but the shuffle mode is not enabled,
+ // will go back to the first music item to play
+ if (mShuffleMode == PlaybackStateCompat.SHUFFLE_MODE_NONE) {
+ mCurrentIndex = 0;
+ } else {
+ // Or will choose a music item from playing list randomly.
+ mCurrentIndex = generateMediaItemIndex();
+ }
+ mCurrentMediaItem = mMediaItemList.get(mCurrentIndex);
+ // The ui will also be changed from playing state to pause state through
+ // setDataSource() operation
+ setDataSource();
+ } else if (mRepeatState == MEDIA_ACTION_REPEAT_ONE) {
+ // Play current music item again.
+ // The ui will stay to be "playing" status for the reason that there is no
+ // setDataSource() function call.
+ mPlayer.start();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PLAYING).build());
+ } else if (mCurrentIndex < mMediaItemList.size() - 1) {
+ if (mShuffleMode == PlaybackStateCompat.SHUFFLE_MODE_NONE) {
+ mCurrentIndex++;
+ } else {
+ mCurrentIndex = generateMediaItemIndex();
+ }
+ mCurrentMediaItem = mMediaItemList.get(mCurrentIndex);
+ // The ui will also be changed from playing state to pause state through
+ // setDataSource() operation
+ setDataSource();
+ } else {
+ // close the service when the playlist is finished
+ // The PlaybackState will be updated to STATE_STOPPED. And onPlayComplete
+ // callback will be called by attached glue.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_STOPPED).build());
+ stopSelf();
+ }
+ }
+ });
+
+ final MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mBufferedProgress = getDuration() * percent / 100;
+ PlaybackStateCompat.Builder builder = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_BUFFERING);
+ builder.setBufferedPosition(mBufferedProgress);
+ mMediaSession.setPlaybackState(builder.build());
+ }
+ };
+ mPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
+ }
+
+
+ /**
+ * Public API to register listener for this service.
+ *
+ * @param listener The listener which will keep tracking current service's status
+ */
+ public void registerCallback(MediaPlayerListener listener) {
+ mCallbacks.add(listener);
+ }
+
+ /**
+ * Instead of shuffling the who music list, we will generate a media item index randomly
+ * and return it as the index for next media item to play.
+ *
+ * @return The index of next media item to play.
+ */
+ private int generateMediaItemIndex() {
+ return new Random().nextInt(mMediaItemList.size());
+ }
+
+ /**
+ * When player is prepared, service will send notification to UI through calling the callback's
+ * method
+ */
+ private void notifyUiWhenPlayerIsPrepared() {
+ for (MediaPlayerListener callback : mCallbacks) {
+ callback.onPrepared();
+ }
+ }
+
+ /**
+ * Set up media session callback to associate with player's operation.
+ */
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ @Override
+ public void onPlay() {
+ play();
+ }
+
+ @Override
+ public void onPause() {
+ pause();
+ }
+
+ @Override
+ public void onSkipToNext() {
+ next();
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ previous();
+ }
+
+ @Override
+ public void onStop() {
+ stop();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ // media player's seekTo method can only take integer as the parameter
+ // so the data type need to be casted as int
+ seekTo((int) pos);
+ }
+
+ @Override
+ public void onFastForward() {
+ fastForward();
+ }
+
+ @Override
+ public void onRewind() {
+ rewind();
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ setRepeatState(repeatMode);
+ }
+
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ setShuffleMode(shuffleMode);
+ }
+ }
+
+ /**
+ * Set new data source and prepare the music player asynchronously.
+ */
+ private void setDataSource() {
+ reset();
+ try {
+ mPlayer.setDataSource(this.getApplicationContext(),
+ mCurrentMediaItem.getMediaSourceUri(getApplicationContext()));
+ mPlayer.prepareAsync();
+ } catch (IOException e) {
+ PlaybackStateCompat.Builder builder = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_ERROR);
+ builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR,
+ CANNOT_SET_DATA_SOURCE);
+ mMediaSession.setPlaybackState(builder.build());
+ }
+ }
+
+ /**
+ * This function will return a playback state builder based on playbackState and current
+ * media position.
+ *
+ * @param playState current playback state.
+ * @return Object of PlaybackStateBuilder.
+ */
+ private PlaybackStateCompat.Builder createPlaybackStateBuilder(int playState) {
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ long currentPosition = getCurrentPosition();
+ float playbackSpeed = NORMAL_SPEED;
+ if (mIsFastForwarding) {
+ playbackSpeed = mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex];
+ // After setting the playback speed, reset mIsFastForwarding flag.
+ mIsFastForwarding = false;
+ } else if (mIsRewinding) {
+ playbackSpeed = mRewindSpeedFactors[mRewindSpeedFactorIndex];
+ // After setting the playback speed, reset mIsRewinding flag.
+ mIsRewinding = false;
+ }
+ playbackStateBuilder.setState(playState, currentPosition, playbackSpeed
+ ).setActions(
+ getPlaybackStateActions()
+ );
+ return playbackStateBuilder;
+ }
+
+ /**
+ * Return supported actions related to current playback state.
+ * Currently the return value from this function is a constant.
+ * For demonstration purpose, the customized fast forward action and customized rewind action
+ * are supported in our case.
+ *
+ * @return playback state actions.
+ */
+ private long getPlaybackStateActions() {
+ long res = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
+ return res;
+ }
+
+ /**
+ * Callback function when media session's meta data is changed.
+ * When this function is returned, the callback function onMetaDataChanged will be
+ * executed to address the new playback state.
+ */
+ private void onMediaSessionMetaDataChanged() {
+ if (mCurrentMediaItem == null) {
+ throw new IllegalArgumentException(
+ "mCurrentMediaItem is null in onMediaSessionMetaDataChanged!");
+ }
+ MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder();
+
+ if (mCurrentMediaItem.getMediaTitle() != null) {
+ metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,
+ mCurrentMediaItem.getMediaTitle());
+ }
+
+ if (mCurrentMediaItem.getMediaDescription() != null) {
+ metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST,
+ mCurrentMediaItem.getMediaDescription());
+ }
+
+ if (mCurrentMediaItem.getMediaAlbumArtResId(getApplicationContext()) != 0) {
+ Bitmap albumArtBitmap = BitmapFactory.decodeResource(getResources(),
+ mCurrentMediaItem.getMediaAlbumArtResId(getApplicationContext()));
+ metaDataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArtBitmap);
+ }
+
+ // duration information will be fetched from player.
+ metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration());
+
+ mMediaSession.setMetadata(metaDataBuilder.build());
+ }
+
+ // Reset player. will be executed when new data source is assigned.
+ private void reset() {
+ if (mPlayer != null) {
+ mPlayer.reset();
+ mInitialized = false;
+ }
+ }
+
+ // Control the player to play the music item.
+ private void play() {
+ // Only when player is not null (meaning the player has been created), the player is
+ // prepared (using the mInitialized as the flag to represent it,
+ // this boolean variable will only be assigned to true inside of the onPrepared callback)
+ // and the media item is not currently playing (!isPlaying()), then the player can be
+ // started.
+
+ // If the player has not been prepared, but this function is fired, it is an error state
+ // from the app side
+ if (!mInitialized) {
+ PlaybackStateCompat.Builder builder = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_ERROR);
+ builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR,
+ PLAYER_NOT_INITIALIZED);
+ mMediaSession.setPlaybackState(builder.build());
+
+ // If the player has is playing, and this function is fired again, it is an error state
+ // from the app side
+ } else {
+ // Request audio focus only when needed
+ if (mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ return;
+ }
+
+ if (mPlayer.getPlaybackParams().getSpeed() != NORMAL_SPEED) {
+ // Reset to normal speed and play
+ resetSpeedAndPlay();
+ } else {
+ // Continue play.
+ mPlayer.start();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PLAYING).build());
+ }
+ }
+
+ }
+
+ // Control the player to pause current music item.
+ private void pause() {
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ // abandon audio focus immediately when the music item is paused.
+ mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener);
+
+ mPlayer.pause();
+ // Update playbackState.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ }
+ }
+
+ // Control the player to stop.
+ private void stop() {
+ if (mPlayer != null) {
+ mPlayer.stop();
+ // Update playbackState.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_STOPPED).build());
+ }
+ }
+
+
+ /**
+ * Control the player to play next music item.
+ * Expected Interaction Behavior:
+ * No matter current media item is playing or not, when use hit next button, next item will be
+ * prepared but won't play unless user hit play button
+ *
+ * Also no matter current media item is fast forwarding or rewinding. Next music item will
+ * be played in normal speed.
+ */
+ private void next() {
+ if (mMediaItemList.isEmpty()) {
+ return;
+ }
+ mCurrentIndex = (mCurrentIndex + 1) % mMediaItemList.size();
+ mCurrentMediaItem = mMediaItemList.get(mCurrentIndex);
+
+ // Reset FastForward/ Rewind state to normal state
+ mFastForwardSpeedFactorIndex = 0;
+ mRewindSpeedFactorIndex = 0;
+ // Set player's playback speed back to normal
+ mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed(
+ mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]));
+ // Pause the player and update the play state.
+ // The ui will also be changed from "playing" state to "pause" state.
+ mPlayer.pause();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ // set new data source to play based on mCurrentIndex and prepare the player.
+ // The ui will also be changed from "playing" state to "pause" state through setDataSource()
+ // operation
+ setDataSource();
+ }
+
+ /**
+ * Control the player to play next music item.
+ * Expected Interaction Behavior:
+ * No matter current media item is playing or not, when use hit previous button, previous item
+ * will be prepared but won't play unless user hit play button
+ *
+ * Also no matter current media item is fast forwarding or rewinding. Previous music item will
+ * be played in normal speed.
+ */
+ private void previous() {
+ if (mMediaItemList.isEmpty()) {
+ return;
+ }
+ mCurrentIndex = (mCurrentIndex - 1 + mMediaItemList.size()) % mMediaItemList.size();
+ mCurrentMediaItem = mMediaItemList.get(mCurrentIndex);
+
+ // Reset FastForward/ Rewind state to normal state
+ mFastForwardSpeedFactorIndex = 0;
+ mRewindSpeedFactorIndex = 0;
+ // Set player's playback speed back to normal
+ mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed(
+ mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]));
+ // Pause the player and update the play state.
+ // The ui will also be changed from "playing" state to "pause" state.
+ mPlayer.pause();
+ // Update playbackState.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ // set new data source to play based on mCurrentIndex and prepare the player.
+ // The ui will also be changed from "playing" state to "pause" state through setDataSource()
+ // operation
+ setDataSource();
+ }
+
+ // Get is playing information from underlying player.
+ private boolean isPlaying() {
+ return mPlayer != null && mPlayer.isPlaying();
+ }
+
+ // Play media item in a fast forward speed.
+ private void fastForward() {
+ // To support fast forward action, the mRewindSpeedFactors must be provided through
+ // setFastForwardSpeedFactors() method;
+ if (mFastForwardSpeedFactors == null) {
+ if (DEBUG) {
+ Log.d(TAG, "FastForwardSpeedFactors are not set");
+ }
+ return;
+ }
+
+ // Toggle the flag to indicate fast forward status.
+ mIsFastForwarding = true;
+
+ // The first element in mFastForwardSpeedFactors is used to represent the normal speed.
+ // Will always be incremented by 1 firstly before setting the speed.
+ mFastForwardSpeedFactorIndex += 1;
+ if (mFastForwardSpeedFactorIndex > mFastForwardSpeedFactors.length - 1) {
+ mFastForwardSpeedFactorIndex = mFastForwardSpeedFactors.length - 1;
+ }
+
+ // In our customized fast forward operation, the media player will not be paused,
+ // But the player's speed will be changed accordingly.
+ mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed(
+ mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]));
+ // Update playback state, mIsFastForwarding will be reset to false inside of it.
+ mMediaSession.setPlaybackState(
+ createPlaybackStateBuilder(PlaybackStateCompat.STATE_FAST_FORWARDING).build());
+ }
+
+
+ // Play media item in a rewind speed.
+ // Android media player doesn't support negative speed. So for customized rewind operation,
+ // the player will be paused internally, but the pause state will not be published. So from
+ // the UI perspective, the player is still in playing status.
+ // Every time when the rewind speed is changed, the position will be computed through previous
+ // rewind speed then media player will seek to that position for seamless playing.
+ private void rewind() {
+ // To support rewind action, the mRewindSpeedFactors must be provided through
+ // setRewindSpeedFactors() method;
+ if (mRewindSpeedFactors == null) {
+ if (DEBUG) {
+ Log.d(TAG, "RewindSpeedFactors are not set");
+ }
+ return;
+ }
+
+ // Perform rewind operation using different speed.
+ if (mIsRewindBegin) {
+ // record end time stamp for previous rewind operation.
+ mRewindEndTime = SystemClock.elapsedRealtime();
+ long position = mRewindStartPosition
+ + (long) mRewindSpeedFactors[mRewindSpeedFactorIndex - 1] * (
+ mRewindEndTime - mRewindStartTime);
+ if (DEBUG) {
+ Log.e(TAG, "Last Rewind Operation Position" + position);
+ }
+ mPlayer.seekTo((int) position);
+
+ // Set new start status
+ mRewindStartPosition = position;
+ mRewindStartTime = mRewindEndTime;
+ // It is still in rewind state, so mIsRewindBegin remains to be true.
+ }
+
+ // Perform rewind operation using the first speed set.
+ if (!mIsRewindBegin) {
+ mRewindStartPosition = getCurrentPosition();
+ Log.e("REWIND_BEGIN", "REWIND BEGIN PLACE " + mRewindStartPosition);
+ mIsRewindBegin = true;
+ mRewindStartTime = SystemClock.elapsedRealtime();
+ }
+
+ // Toggle the flag to indicate rewind status.
+ mIsRewinding = true;
+
+ // Pause the player but won't update the UI status.
+ mPlayer.pause();
+
+ // Update playback state, mIsRewinding will be reset to false inside of it.
+ mMediaSession.setPlaybackState(
+ createPlaybackStateBuilder(PlaybackStateCompat.STATE_REWINDING).build());
+
+ mRewindSpeedFactorIndex += 1;
+ if (mRewindSpeedFactorIndex > mRewindSpeedFactors.length - 1) {
+ mRewindSpeedFactorIndex = mRewindSpeedFactors.length - 1;
+ }
+ }
+
+ // Reset the playing speed to normal.
+ // From PlaybackBannerGlue's key dispatching mechanism. If the player is currently in rewinding
+ // or fast forwarding status, moving from the rewinding/ FastForwarindg button will trigger
+ // the fastForwarding/ rewinding ending event.
+ // When customized fast forwarding or rewinding actions are supported, this function will be
+ // called.
+ // If we are in rewind mode, this function will compute the new position through rewinding
+ // speed and compare the start/ end rewinding time stamp.
+ private void resetSpeedAndPlay() {
+
+ if (mIsRewindBegin) {
+ mIsRewindBegin = false;
+ mRewindEndTime = SystemClock.elapsedRealtime();
+
+ long position = mRewindStartPosition
+ + (long) mRewindSpeedFactors[mRewindSpeedFactorIndex ] * (
+ mRewindEndTime - mRewindStartTime);
+
+ // Seek to the computed position for seamless playing.
+ mPlayer.seekTo((int) position);
+ }
+
+ // Reset the state to normal state.
+ mFastForwardSpeedFactorIndex = 0;
+ mRewindSpeedFactorIndex = 0;
+ mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed(
+ mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]));
+
+ // Update the playback status from rewinding/ fast forwardindg to STATE_PLAYING.
+ // Which indicates current media item is played in the normal speed.
+ mMediaSession.setPlaybackState(
+ createPlaybackStateBuilder(PlaybackStateCompat.STATE_PLAYING).build());
+ }
+
+ // Get current playing progress from media player.
+ private int getCurrentPosition() {
+ if (mInitialized && mPlayer != null) {
+ // Always record current position for seekTo operation.
+ mCurrentPosition = mPlayer.getCurrentPosition();
+ return mPlayer.getCurrentPosition();
+ }
+ return 0;
+ }
+
+ // get music duration from underlying music player
+ private int getDuration() {
+ return (mInitialized && mPlayer != null) ? mPlayer.getDuration() : 0;
+ }
+
+ // seek to specific position through underlying music player.
+ private void seekTo(int newPosition) {
+ if (mPlayer != null) {
+ mPlayer.seekTo(newPosition);
+ }
+ }
+
+ // set shuffle mode through passed parameter.
+ private void setShuffleMode(int shuffleMode) {
+ mShuffleMode = shuffleMode;
+ }
+
+ // set shuffle mode through passed parameter.
+ public void setRepeatState(int repeatState) {
+ mRepeatState = repeatState;
+ }
+
+ private void audioFocusLossHandler() {
+ // Permanent loss of audio focus
+ // Pause playback immediately
+ mPlayer.pause();
+ // Wait 30 seconds before stopping playback
+ mMediaPlayerHandler.postDelayed(mDelayedStopRunnable, 30);
+ // Update playback state.
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ // Will record current player progress when losing the audio focus.
+ mCurrentPosition = getCurrentPosition();
+ }
+
+ private void audioLossFocusTransientHandler() {
+ // In this case, we already have lost the audio focus, and we cannot duck.
+ // So the player will be paused immediately, but different with the previous state, there is
+ // no need to stop the player.
+ mPlayer.pause();
+ // update playback state
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PAUSED).build());
+ // Will record current player progress when lossing the audio focus.
+ mCurrentPosition = getCurrentPosition();
+ }
+
+ private void audioLossFocusTransientCanDuckHanlder() {
+ // In this case, we have lots the audio focus, but since we can duck
+ // the music item can continue to play but the volume will be reduced
+ mPlayer.setVolume(REDUCED_VOLUME, REDUCED_VOLUME);
+ }
+
+ private void audioFocusGainHandler() {
+ // In this case the app has been granted audio focus again
+ // Firstly, raise volume to normal
+ mPlayer.setVolume(FULL_VOLUME, FULL_VOLUME);
+
+ // If the recorded position is the same as current position
+ // Start the player directly
+ if (mCurrentPosition == mPlayer.getCurrentPosition()) {
+ mPlayer.start();
+ mMediaSession.setPlaybackState(createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_PLAYING).build());
+ // If the recorded position is not equal to current position
+ // The player will seek to the last recorded position firstly to continue playing the
+ // last music item
+ } else {
+ mPlayer.seekTo(mCurrentPosition);
+ PlaybackStateCompat.Builder builder = createPlaybackStateBuilder(
+ PlaybackStateCompat.STATE_BUFFERING);
+ builder.setBufferedPosition(mBufferedProgress);
+ mMediaSession.setPlaybackState(builder.build());
+ }
+ }
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicExampleActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicExampleActivity.java
new file mode 100644
index 0000000..0b07248
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicExampleActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 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.example.android.leanback;
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Main Activity for the Music Player
+ */
+public class MusicExampleActivity extends Activity {
+
+ @Override public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_music_example);
+ }
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicItem.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicItem.java
new file mode 100644
index 0000000..073cf80
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicItem.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2015 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.example.android.leanback;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Abstract data type to represent music item.
+ */
+public class MusicItem {
+ // Duration information of current.
+ @SerializedName("duration")
+ private String mDuration;
+
+ // File name of this media item.
+ @SerializedName("file")
+ private String mFile;
+
+ // The title of this media item.
+ @SerializedName("title")
+ private String mMediaTitle;
+
+ // The description of media item.
+ @SerializedName("description")
+ private String mDescription;
+
+ // Art information (i.e. cover image) of this media item.
+ @SerializedName("art")
+ private String mArt;
+
+ /**
+ * The conversion function which can return media item's uri through the file name.
+ *
+ * @param context The context used to get resources of this app.
+ * @return The Uri of the selected media item.
+ */
+ public Uri getMediaSourceUri(Context context) {
+ return getResourceUri(context, context.getResources()
+ .getIdentifier(mFile, "raw", context.getPackageName()));
+ }
+
+ /**
+ * Return the title of current media item.
+ *
+ * @return The title of current media item.
+ */
+ public String getMediaTitle() {
+ return mMediaTitle;
+ }
+
+ /**
+ * Return the description of current media item.
+ *
+ * @return The description of current media item.
+ */
+ public String getMediaDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Return the resource id through art file's name.
+ *
+ * @param context The context used to get resources of this app.
+ * @return The resource Id of the selected media item.
+ */
+ public int getMediaAlbumArtResId(Context context) {
+ return context.getResources()
+ .getIdentifier(mArt, "drawable", context.getPackageName());
+ }
+
+ /**
+ * Helper function to get resource uri based on android resource scheme
+ *
+ * @param context Context to get resources.
+ * @param resID Resource ID.
+ * @return The Uri of current resource.
+ */
+ public static Uri getResourceUri(Context context, int resID) {
+ return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ + "://"
+ + context.getResources().getResourcePackageName(resID)
+ + '/'
+ + context.getResources().getResourceTypeName(resID)
+ + '/'
+ + context.getResources().getResourceEntryName(resID));
+ }
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicPlayerFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicPlayerFragment.java
new file mode 100644
index 0000000..5948bc2
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MusicPlayerFragment.java
@@ -0,0 +1,255 @@
+/*
+ * 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.example.android.leanback;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v17.leanback.app.PlaybackFragment;
+import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
+import android.support.v17.leanback.media.MediaControllerAdapter;
+import android.support.v17.leanback.media.PlaybackBannerControlGlue;
+import android.support.v17.leanback.media.PlaybackBaseControlGlue;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import com.google.gson.Gson;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The fragment which contains the MediaSessionService through binding to it.
+ * Also this fragment contains a specialized glue with repeat and shuffle operation.
+ */
+public class MusicPlayerFragment extends PlaybackFragment implements
+ MediaSessionService.MediaPlayerListener {
+
+ /**
+ * For this app, when the player is prepared, the music item will not be played automatically,
+ * so we will fire pause() operation here through attached glue in case current play back
+ * state is playing.
+ */
+ @Override
+ public void onPrepared() {
+ mGlue.pause();
+ }
+
+ /**
+ * This control glue is extended from {@link PlaybackBannerControlGlue} for two additional
+ * operation (shuffle and repeat)
+ * Also, a secondary control row will be added in this glue to hold these two operations.
+ *
+ * @param <T> T extends MediaControllerAdapter
+ */
+ private class PlaybackBannerMusicPlayerControlGlue<T extends MediaControllerAdapter> extends
+ PlaybackBannerControlGlue<T> {
+
+ // Two more action (Repeat and Shuffle) is added to demonstrate the usage of the API defined
+ // in MediaControllerAdapter
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ShuffleAction mShuffleAction;
+
+ private PlaybackBannerMusicPlayerControlGlue(Context context, int[] fastForwardSpeeds,
+ int[] rewindSpeeds, T impl) {
+ super(context, fastForwardSpeeds, rewindSpeeds, impl);
+ }
+
+ /**
+ * Create secondary control row to hold repeat and shuffle action.
+ *
+ * @param secondaryActionsAdapter The adapter you need to add the {@link Action}s to.
+ */
+ @Override
+ protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
+ final long supportedActions = getSupportedActions();
+
+ if ((supportedActions & ACTION_REPEAT) != 0 && mRepeatAction == null) {
+ secondaryActionsAdapter.add(
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(getContext()));
+ } else if ((supportedActions & ACTION_REPEAT) == 0
+ && mRepeatAction != null) {
+ secondaryActionsAdapter.remove(mRepeatAction);
+ mRepeatAction = null;
+ }
+
+ if ((supportedActions & ACTION_SHUFFLE) != 0 && mShuffleAction == null) {
+ secondaryActionsAdapter.add(
+ mShuffleAction = new PlaybackControlsRow.ShuffleAction(getContext()));
+ } else if ((supportedActions & ACTION_SHUFFLE) == 0
+ && mShuffleAction != null) {
+ secondaryActionsAdapter.remove(mShuffleAction);
+ mShuffleAction = null;
+ }
+ }
+
+ /**
+ * Media art, title and subtitle will be updated every time when this callback
+ * function is called.
+ */
+ @Override
+ public void onMetadataChanged() {
+ super.onMetadataChanged();
+ setArt(getPlayerAdapter().getMediaArt(getActivity()));
+ setTitle(getPlayerAdapter().getMediaTitle());
+ setSubtitle(getPlayerAdapter().getMediaSubtitle());
+ }
+
+ @Override
+ public long getSupportedActions() {
+ long supportedActions = super.getSupportedActions();
+ // In our case, if fast forward action or rewind action are not supported by adapter
+ // "Fake Fast Forward" and "Fake Rewind" will be executed when user press fast forward
+ // or rewind button.
+ return supportedActions
+ | PlaybackBaseControlGlue.ACTION_FAST_FORWARD
+ | PlaybackBaseControlGlue.ACTION_REWIND;
+ }
+
+ /**
+ * The callback function to dispatch the action.
+ *
+ * @param action Action performed by app user.
+ */
+ @Override
+ public void onActionClicked(Action action) {
+ // In our customized glue, only shuffle and repeat these two actions will be
+ // processed specifically. Other actions will be handled by super method.
+ super.onActionClicked(action);
+
+ // when the action is an instance of repeat action
+ if (action instanceof PlaybackControlsRow.RepeatAction) {
+ PlaybackControlsRow.RepeatAction repeatAction =
+ ((PlaybackControlsRow.RepeatAction) action);
+ repeatAction.nextIndex();
+ int index = (repeatAction).getIndex();
+ if (getPlayerAdapter() != null) {
+ getPlayerAdapter().setRepeatAction(index);
+ }
+ notifyItemChanged(
+ (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter(),
+ action);
+ }
+
+ // when the action is an instance of shuffle action
+ if (action instanceof PlaybackControlsRow.ShuffleAction) {
+ PlaybackControlsRow.ShuffleAction shuffleAction =
+ ((PlaybackControlsRow.ShuffleAction) action);
+ shuffleAction.nextIndex();
+ int index = (shuffleAction).getIndex();
+ if (getPlayerAdapter() != null) {
+ getPlayerAdapter().setShuffleAction(index);
+ }
+ notifyItemChanged(
+ (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter(), action);
+ }
+ }
+ }
+
+ private PlaybackBannerMusicPlayerControlGlue<MediaControllerAdapter> mGlue;
+ private MediaControllerCompat mController;
+ private MediaControllerAdapter mAdapter;
+ private MediaSessionCompat mMediaSession;
+ private MediaSessionService mPlaybackService;
+ private List<MusicItem> mSongMetaDataList = new ArrayList<>();
+
+ // when the service is bound, fragment will get media session's instance
+ private ServiceConnection mPlaybackServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ int[] fastForwardSpeed = new int[]{2, 3, 4, 5};
+ int[] rewindSpeed = new int[]{2, 3, 4, 5};
+
+ // Bind the service.
+ MediaSessionService.LocalBinder binder = (MediaSessionService.LocalBinder) iBinder;
+ mPlaybackService = binder.getService();
+
+ // Register this fragment as the UI callback for backend service.
+ mPlaybackService.registerCallback(MusicPlayerFragment.this);
+
+ // When the service is created, the video player/ audio manager will be initialized
+ // The following method will create the media item list and set the data source to
+ // the first item in current media list. Parameter false means, the original media
+ // item lists will be removed.
+ mPlaybackService.setMediaList(mSongMetaDataList, false);
+
+ // Set FastForward and Rewind Speed Factors.
+ mPlaybackService.setFastForwardSpeedFactors(fastForwardSpeed);
+ mPlaybackService.setRewindSpeedFactors(rewindSpeed);
+
+ // Get media session through service
+ mMediaSession = mPlaybackService.getMediaSession();
+ // The adapter is created using current MediaSession
+ mController = new MediaControllerCompat(MusicPlayerFragment.this.getActivity(),
+ mMediaSession);
+ mAdapter = new MediaControllerAdapter(mController);
+ mGlue = new PlaybackBannerMusicPlayerControlGlue<>(getActivity(), fastForwardSpeed,
+ rewindSpeed,
+ mAdapter);
+
+ // register this callback in service, so current UI can toggle UI pause
+ // to control control row's status
+ mGlue.setHost(new PlaybackFragmentGlueHost(MusicPlayerFragment.this));
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mPlaybackService = null;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String json = inputStreamToString(
+ getActivity().getResources().openRawResource(R.raw.media));
+ MusicItem[] musicItems = new Gson().fromJson(json, MusicItem[].class);
+ for (MusicItem i : musicItems) {
+ mSongMetaDataList.add(i);
+ }
+ Intent serviceIntent = new Intent(getActivity(), MediaSessionService.class);
+ getActivity().bindService(serviceIntent, mPlaybackServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Helper function to read the content from a given {@link InputStream} and return it as a
+ * {@link String}.
+ *
+ * @param inputStream The {@link InputStream} which should be read.
+ * @return Returns <code>null</code> if the the {@link InputStream} could not be read. Else
+ * returns the content of the {@link InputStream} as {@link String}.
+ */
+ private static String inputStreamToString(InputStream inputStream) {
+ try {
+ byte[] bytes = new byte[inputStream.available()];
+ inputStream.read(bytes, 0, bytes.length);
+ String json = new String(bytes);
+ return json;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/v17/leanback/api/27.0.0-SNAPSHOT.txt b/v17/leanback/api/27.0.0-SNAPSHOT.txt
index fb2aebc..2b0d6d8 100644
--- a/v17/leanback/api/27.0.0-SNAPSHOT.txt
+++ b/v17/leanback/api/27.0.0-SNAPSHOT.txt
@@ -1147,6 +1147,16 @@
package android.support.v17.leanback.media {
+ public class MediaControllerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
+ ctor public MediaControllerAdapter(android.support.v4.media.session.MediaControllerCompat);
+ method public android.graphics.drawable.Drawable getMediaArt(android.content.Context);
+ method public android.support.v4.media.session.MediaControllerCompat getMediaController();
+ method public java.lang.CharSequence getMediaSubtitle();
+ method public java.lang.CharSequence getMediaTitle();
+ method public void pause();
+ method public void play();
+ }
+
public abstract class MediaControllerGlue extends android.support.v17.leanback.media.PlaybackControlGlue {
ctor public MediaControllerGlue(android.content.Context, int[], int[]);
method public void attachToMediaController(android.support.v4.media.session.MediaControllerCompat);
@@ -1180,7 +1190,6 @@
ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T);
method public int[] getFastForwardSpeeds();
method public int[] getRewindSpeeds();
- method public long getSupportedActions();
method public void onActionClicked(android.support.v17.leanback.widget.Action);
method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
method public boolean onKey(android.view.View, int, android.view.KeyEvent);
@@ -1211,6 +1220,7 @@
method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
method public final T getPlayerAdapter();
method public java.lang.CharSequence getSubtitle();
+ method public long getSupportedActions();
method public java.lang.CharSequence getTitle();
method public boolean isControlsOverlayAutoHideEnabled();
method public final boolean isPlaying();
@@ -1221,6 +1231,7 @@
method protected abstract android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method protected void onMetadataChanged();
method protected void onPlayCompleted();
method protected void onPlayStateChanged();
method protected void onPreparedStateChanged();
@@ -1231,6 +1242,15 @@
method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
method public void setSubtitle(java.lang.CharSequence);
method public void setTitle(java.lang.CharSequence);
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REPEAT = 512; // 0x200
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SHUFFLE = 1024; // 0x400
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
}
public abstract class PlaybackControlGlue extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
@@ -1363,19 +1383,26 @@
public abstract class PlayerAdapter {
ctor public PlayerAdapter();
+ method public void fastForward();
method public long getBufferedPosition();
method public final android.support.v17.leanback.media.PlayerAdapter.Callback getCallback();
method public long getCurrentPosition();
method public long getDuration();
+ method public long getSupportedActions();
method public boolean isPlaying();
method public boolean isPrepared();
+ method public void next();
method public void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
method public void onDetachedFromHost();
method public abstract void pause();
method public abstract void play();
+ method public void previous();
+ method public void rewind();
method public void seekTo(long);
method public final void setCallback(android.support.v17.leanback.media.PlayerAdapter.Callback);
method public void setProgressUpdatingEnabled(boolean);
+ method public void setRepeatAction(int);
+ method public void setShuffleAction(int);
}
public static class PlayerAdapter.Callback {
@@ -1385,6 +1412,7 @@
method public void onCurrentPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onDurationChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onError(android.support.v17.leanback.media.PlayerAdapter, int, java.lang.String);
+ method public void onMetadataChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onPlayCompleted(android.support.v17.leanback.media.PlayerAdapter);
method public void onPlayStateChanged(android.support.v17.leanback.media.PlayerAdapter);
method public void onPreparedStateChanged(android.support.v17.leanback.media.PlayerAdapter);
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java b/v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
new file mode 100644
index 0000000..5993e35
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
@@ -0,0 +1,348 @@
+/*
+ * 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 android.support.v17.leanback.media;
+
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_FAST_FORWARD;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_PLAY_PAUSE;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_REPEAT;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_REWIND;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_SHUFFLE;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT;
+import static android.support.v17.leanback.media.PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+/**
+ * A helper class for implementing a adapter layer for {@link MediaControllerCompat}.
+ */
+public class MediaControllerAdapter extends PlayerAdapter {
+
+ private static final String TAG = "MediaControllerAdapter";
+ private static final boolean DEBUG = false;
+
+ private MediaControllerCompat mController;
+ private Handler mHandler = new Handler();
+
+ // Runnable object to update current media's playing position.
+ private final Runnable mPositionUpdaterRunnable = new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onCurrentPositionChanged(MediaControllerAdapter.this);
+ mHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+
+ // Update period to post runnable.
+ private int getUpdatePeriod() {
+ return 16;
+ }
+
+ private boolean mIsBuffering = false;
+
+ MediaControllerCompat.Callback mMediaControllerCallback =
+ new MediaControllerCompat.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ if (mIsBuffering && state.getState() != PlaybackStateCompat.STATE_BUFFERING) {
+ getCallback().onBufferingStateChanged(MediaControllerAdapter.this, false);
+ getCallback().onBufferedPositionChanged(MediaControllerAdapter.this);
+ mIsBuffering = false;
+ }
+ if (state.getState() == PlaybackStateCompat.STATE_NONE) {
+ // The STATE_NONE playback state will only occurs when initialize the player
+ // at first time.
+ if (DEBUG) {
+ Log.d(TAG, "Playback state is none");
+ }
+ } else if (state.getState() == PlaybackStateCompat.STATE_STOPPED) {
+ // STATE_STOPPED is associated with onPlayCompleted() callback.
+ // STATE_STOPPED playback state will only occurs when the last item in
+ // play list is finished. And repeat mode is not enabled.
+ getCallback().onPlayCompleted(MediaControllerAdapter.this);
+ } else if (state.getState() == PlaybackStateCompat.STATE_PAUSED) {
+ getCallback().onPlayStateChanged(MediaControllerAdapter.this);
+ getCallback().onCurrentPositionChanged(MediaControllerAdapter.this);
+ } else if (state.getState() == PlaybackStateCompat.STATE_PLAYING) {
+ getCallback().onPlayStateChanged(MediaControllerAdapter.this);
+ getCallback().onCurrentPositionChanged(MediaControllerAdapter.this);
+ } else if (state.getState() == PlaybackStateCompat.STATE_BUFFERING) {
+ mIsBuffering = true;
+ getCallback().onBufferingStateChanged(MediaControllerAdapter.this, true);
+ getCallback().onBufferedPositionChanged(MediaControllerAdapter.this);
+ } else if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
+ CharSequence errorMessage = state.getErrorMessage();
+ if (errorMessage == null) {
+ getCallback().onError(MediaControllerAdapter.this, state.getErrorCode(),
+ "");
+ } else {
+ getCallback().onError(MediaControllerAdapter.this, state.getErrorCode(),
+ state.getErrorMessage().toString());
+ }
+ } else if (state.getState() == PlaybackStateCompat.STATE_FAST_FORWARDING) {
+ getCallback().onPlayStateChanged(MediaControllerAdapter.this);
+ getCallback().onCurrentPositionChanged(MediaControllerAdapter.this);
+ } else if (state.getState() == PlaybackStateCompat.STATE_REWINDING) {
+ getCallback().onPlayStateChanged(MediaControllerAdapter.this);
+ getCallback().onCurrentPositionChanged(MediaControllerAdapter.this);
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ getCallback().onMetadataChanged(MediaControllerAdapter.this);
+ }
+ };
+
+ /**
+ * Constructor for the adapter using {@link MediaControllerCompat}.
+ *
+ * @param controller Object of MediaControllerCompat..
+ */
+ public MediaControllerAdapter(MediaControllerCompat controller) {
+ if (controller == null) {
+ throw new NullPointerException("Object of MediaControllerCompat is null");
+ }
+ mController = controller;
+ }
+
+ /**
+ * Return the object of {@link MediaControllerCompat} from this class.
+ *
+ * @return Media Controller Compat object owned by this class.
+ */
+ public MediaControllerCompat getMediaController() {
+ return mController;
+ }
+
+ @Override
+ public void play() {
+ mController.getTransportControls().play();
+ }
+
+ @Override
+ public void pause() {
+ mController.getTransportControls().pause();
+ }
+
+ @Override
+ public void seekTo(long positionInMs) {
+ mController.getTransportControls().seekTo(positionInMs);
+ }
+
+ @Override
+ public void next() {
+ mController.getTransportControls().skipToNext();
+ }
+
+ @Override
+ public void previous() {
+ mController.getTransportControls().skipToPrevious();
+ }
+
+ @Override
+ public void fastForward() {
+ mController.getTransportControls().fastForward();
+ }
+
+ @Override
+ public void rewind() {
+ mController.getTransportControls().rewind();
+ }
+
+ @Override
+ public void setRepeatAction(int repeatActionIndex) {
+ int repeatMode = mapRepeatActionToRepeatMode(repeatActionIndex);
+ mController.getTransportControls().setRepeatMode(repeatMode);
+ }
+
+ @Override
+ public void setShuffleAction(int shuffleActionIndex) {
+ int shuffleMode = mapShuffleActionToShuffleMode(shuffleActionIndex);
+ mController.getTransportControls().setShuffleMode(shuffleMode);
+ }
+
+ @Override
+ public boolean isPlaying() {
+ if (mController.getPlaybackState() == null) {
+ return false;
+ }
+ return mController.getPlaybackState().getState()
+ == PlaybackStateCompat.STATE_PLAYING
+ || mController.getPlaybackState().getState()
+ == PlaybackStateCompat.STATE_FAST_FORWARDING
+ || mController.getPlaybackState().getState() == PlaybackStateCompat.STATE_REWINDING;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ if (mController.getPlaybackState() == null) {
+ return 0;
+ }
+ return mController.getPlaybackState().getPosition();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ if (mController.getPlaybackState() == null) {
+ return 0;
+ }
+ return mController.getPlaybackState().getBufferedPosition();
+ }
+
+ /**
+ * Get current media's title.
+ *
+ * @return Title of current media.
+ */
+ public CharSequence getMediaTitle() {
+ if (mController.getMetadata() == null) {
+ return "";
+ }
+ return mController.getMetadata().getDescription().getTitle();
+ }
+
+ /**
+ * Get current media's subtitle.
+ *
+ * @return Subtitle of current media.
+ */
+ public CharSequence getMediaSubtitle() {
+ if (mController.getMetadata() == null) {
+ return "";
+ }
+ return mController.getMetadata().getDescription().getSubtitle();
+ }
+
+ /**
+ * Get current media's drawable art.
+ *
+ * @return Drawable art of current media.
+ */
+ public Drawable getMediaArt(Context context) {
+ if (mController.getMetadata() == null) {
+ return null;
+ }
+ Bitmap bitmap = mController.getMetadata().getDescription().getIconBitmap();
+ return bitmap == null ? null : new BitmapDrawable(context.getResources(), bitmap);
+ }
+
+ @Override
+ public long getDuration() {
+ if (mController.getMetadata() == null) {
+ return 0;
+ }
+ return (int) mController.getMetadata().getLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION);
+ }
+
+ @Override
+ public void onAttachedToHost(PlaybackGlueHost host) {
+ mController.registerCallback(mMediaControllerCallback);
+ }
+
+ @Override
+ public void onDetachedFromHost() {
+ mController.unregisterCallback(mMediaControllerCallback);
+ }
+
+ @Override
+ public void setProgressUpdatingEnabled(boolean enabled) {
+ mHandler.removeCallbacks(mPositionUpdaterRunnable);
+ if (!enabled) {
+ return;
+ }
+ mHandler.postDelayed(mPositionUpdaterRunnable, getUpdatePeriod());
+ }
+
+ @Override
+ public long getSupportedActions() {
+ long supportedActions = 0;
+ if (mController.getPlaybackState() == null) {
+ return supportedActions;
+ }
+ long actionsFromController = mController.getPlaybackState().getActions();
+ // Translation.
+ if ((actionsFromController & PlaybackStateCompat.ACTION_PLAY_PAUSE) != 0) {
+ supportedActions |= ACTION_PLAY_PAUSE;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) {
+ supportedActions |= ACTION_SKIP_TO_NEXT;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) {
+ supportedActions |= ACTION_SKIP_TO_PREVIOUS;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) {
+ supportedActions |= ACTION_FAST_FORWARD;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_REWIND) != 0) {
+ supportedActions |= ACTION_REWIND;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_SET_REPEAT_MODE) != 0) {
+ supportedActions |= ACTION_REPEAT;
+ }
+ if ((actionsFromController & PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED) != 0) {
+ supportedActions |= ACTION_SHUFFLE;
+ }
+ return supportedActions;
+ }
+
+ /**
+ * This function will translate the index of RepeatAction in PlaybackControlsRow to
+ * the repeat mode which is defined by PlaybackStateCompat.
+ *
+ * @param repeatActionIndex Index of RepeatAction in PlaybackControlsRow.
+ * @return Repeat Mode in playback state.
+ */
+ private int mapRepeatActionToRepeatMode(int repeatActionIndex) {
+ switch (repeatActionIndex) {
+ case PlaybackControlsRow.RepeatAction.INDEX_NONE:
+ return PlaybackStateCompat.REPEAT_MODE_NONE;
+ case PlaybackControlsRow.RepeatAction.INDEX_ALL:
+ return PlaybackStateCompat.REPEAT_MODE_ALL;
+ case PlaybackControlsRow.RepeatAction.INDEX_ONE:
+ return PlaybackStateCompat.REPEAT_MODE_ONE;
+ }
+ return -1;
+ }
+
+ /**
+ * This function will translate the index of RepeatAction in PlaybackControlsRow to
+ * the repeat mode which is defined by PlaybackStateCompat.
+ *
+ * @param shuffleActionIndex Index of RepeatAction in PlaybackControlsRow.
+ * @return Repeat Mode in playback state.
+ */
+ private int mapShuffleActionToShuffleMode(int shuffleActionIndex) {
+ switch (shuffleActionIndex) {
+ case PlaybackControlsRow.ShuffleAction.INDEX_OFF:
+ return PlaybackStateCompat.SHUFFLE_MODE_NONE;
+ case PlaybackControlsRow.ShuffleAction.INDEX_ON:
+ return PlaybackStateCompat.SHUFFLE_MODE_ALL;
+ }
+ return -1;
+ }
+}
+
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
index bcf5945..ca424a8 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
@@ -84,38 +84,41 @@
* The adapter key for the first custom control on the left side
* of the predefined primary controls.
*/
- public static final int ACTION_CUSTOM_LEFT_FIRST = 0x1;
+ public static final int ACTION_CUSTOM_LEFT_FIRST =
+ PlaybackBaseControlGlue.ACTION_CUSTOM_LEFT_FIRST;
/**
* The adapter key for the skip to previous control.
*/
- public static final int ACTION_SKIP_TO_PREVIOUS = 0x10;
+ public static final int ACTION_SKIP_TO_PREVIOUS =
+ PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS;
/**
* The adapter key for the rewind control.
*/
- public static final int ACTION_REWIND = 0x20;
+ public static final int ACTION_REWIND = PlaybackBaseControlGlue.ACTION_REWIND;
/**
* The adapter key for the play/pause control.
*/
- public static final int ACTION_PLAY_PAUSE = 0x40;
+ public static final int ACTION_PLAY_PAUSE = PlaybackBaseControlGlue.ACTION_PLAY_PAUSE;
/**
* The adapter key for the fast forward control.
*/
- public static final int ACTION_FAST_FORWARD = 0x80;
+ public static final int ACTION_FAST_FORWARD = PlaybackBaseControlGlue.ACTION_FAST_FORWARD;
/**
* The adapter key for the skip to next control.
*/
- public static final int ACTION_SKIP_TO_NEXT = 0x100;
+ public static final int ACTION_SKIP_TO_NEXT = PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT;
/**
* The adapter key for the first custom control on the right side
* of the predefined primary controls.
*/
- public static final int ACTION_CUSTOM_RIGHT_FIRST = 0x1000;
+ public static final int ACTION_CUSTOM_RIGHT_FIRST =
+ PlaybackBaseControlGlue.ACTION_CUSTOM_RIGHT_FIRST;
/** @hide */
@@ -195,6 +198,12 @@
private long mStartTime;
private long mStartPosition = 0;
+ // Flag for is customized FastForward/ Rewind Action supported.
+ // If customized actions are not supported, the adapter can still use default behavior through
+ // setting ACTION_REWIND and ACTION_FAST_FORWARD as supported actions.
+ private boolean mIsCustomizedFastForwardSupported;
+ private boolean mIsCustomizedRewindSupported;
+
/**
* Constructor for the glue.
*
@@ -234,6 +243,12 @@
throw new IllegalArgumentException("invalid rewindSpeeds array size");
}
mRewindSpeeds = rewindSpeeds;
+ if ((mPlayerAdapter.getSupportedActions() & ACTION_FAST_FORWARD) != 0) {
+ mIsCustomizedFastForwardSupported = true;
+ }
+ if ((mPlayerAdapter.getSupportedActions() & ACTION_REWIND) != 0) {
+ mIsCustomizedRewindSupported = true;
+ }
}
@Override
@@ -368,6 +383,38 @@
updatePlaybackState(mIsPlaying);
}
+ // Helper function to increment mPlaybackSpeed when necessary. The mPlaybackSpeed will control
+ // the UI of fast forward button in control row.
+ private void incrementFastForwardPlaybackSpeed() {
+ switch (mPlaybackSpeed) {
+ case PLAYBACK_SPEED_FAST_L0:
+ case PLAYBACK_SPEED_FAST_L1:
+ case PLAYBACK_SPEED_FAST_L2:
+ case PLAYBACK_SPEED_FAST_L3:
+ mPlaybackSpeed++;
+ break;
+ default:
+ mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0;
+ break;
+ }
+ }
+
+ // Helper function to decrement mPlaybackSpeed when necessary. The mPlaybackSpeed will control
+ // the UI of rewind button in control row.
+ private void decrementRewindPlaybackSpeed() {
+ switch (mPlaybackSpeed) {
+ case -PLAYBACK_SPEED_FAST_L0:
+ case -PLAYBACK_SPEED_FAST_L1:
+ case -PLAYBACK_SPEED_FAST_L2:
+ case -PLAYBACK_SPEED_FAST_L3:
+ mPlaybackSpeed--;
+ break;
+ default:
+ mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0;
+ break;
+ }
+ }
+
/**
* Called when the given action is invoked, either by click or key event.
*/
@@ -401,37 +448,37 @@
handled = true;
} else if (action == mFastForwardAction) {
if (mPlayerAdapter.isPrepared() && mPlaybackSpeed < getMaxForwardSpeedId()) {
- fakePause();
-
- switch (mPlaybackSpeed) {
- case PLAYBACK_SPEED_FAST_L0:
- case PLAYBACK_SPEED_FAST_L1:
- case PLAYBACK_SPEED_FAST_L2:
- case PLAYBACK_SPEED_FAST_L3:
- mPlaybackSpeed++;
- break;
- default:
- mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0;
- break;
+ // When the customized fast forward action is available, it will be executed
+ // when fast forward button is pressed. If current media item is not playing, the UI
+ // will be updated to PLAYING status.
+ if (mIsCustomizedFastForwardSupported) {
+ // Change UI to Playing status.
+ mIsPlaying = true;
+ // Execute customized fast forward action.
+ mPlayerAdapter.fastForward();
+ } else {
+ // When the customized fast forward action is not supported, the fakePause
+ // operation is needed to stop the media item but still indicating the media
+ // item is playing from the UI perspective
+ // Also the fakePause() method must be called before
+ // incrementFastForwardPlaybackSpeed() method to make sure fake fast forward
+ // computation is accurate.
+ fakePause();
}
+ // Change mPlaybackSpeed to control the UI.
+ incrementFastForwardPlaybackSpeed();
onUpdatePlaybackStatusAfterUserAction();
}
handled = true;
} else if (action == mRewindAction) {
if (mPlayerAdapter.isPrepared() && mPlaybackSpeed > -getMaxRewindSpeedId()) {
- fakePause();
-
- switch (mPlaybackSpeed) {
- case -PLAYBACK_SPEED_FAST_L0:
- case -PLAYBACK_SPEED_FAST_L1:
- case -PLAYBACK_SPEED_FAST_L2:
- case -PLAYBACK_SPEED_FAST_L3:
- mPlaybackSpeed--;
- break;
- default:
- mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0;
- break;
+ if (mIsCustomizedFastForwardSupported) {
+ mIsPlaying = true;
+ mPlayerAdapter.rewind();
+ } else {
+ fakePause();
}
+ decrementRewindPlaybackSpeed();
onUpdatePlaybackStatusAfterUserAction();
}
handled = true;
@@ -551,9 +598,19 @@
// If the adapter is playing/paused, using the position from adapter instead.
return mPlayerAdapter.getCurrentPosition();
} else if (mPlaybackSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ // If fast forward operation is supported in this scenario, current player position
+ // can be get from mPlayerAdapter.getCurrentPosition() directly
+ if (mIsCustomizedFastForwardSupported) {
+ return mPlayerAdapter.getCurrentPosition();
+ }
int index = mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
speed = getFastForwardSpeeds()[index];
} else if (mPlaybackSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ // If fast rewind is supported in this scenario, current player position
+ // can be get from mPlayerAdapter.getCurrentPosition() directly
+ if (mIsCustomizedRewindSupported) {
+ return mPlayerAdapter.getCurrentPosition();
+ }
int index = -mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
speed = -getRewindSpeeds()[index];
} else {
@@ -577,13 +634,6 @@
return position;
}
- /**
- * Returns a bitmask of actions supported by the media player.
- * @see ACTION_ for constants that may be returned by this method.
- */
- public long getSupportedActions() {
- return PlaybackBannerControlGlue.ACTION_PLAY_PAUSE;
- }
@Override
public void play() {
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
index 24b2dcb..ef799b2 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
@@ -61,6 +61,53 @@
public abstract class PlaybackBaseControlGlue<T extends PlayerAdapter> extends PlaybackGlue
implements OnActionClickedListener, View.OnKeyListener {
+ /**
+ * The adapter key for the first custom control on the left side
+ * of the predefined primary controls.
+ */
+ public static final int ACTION_CUSTOM_LEFT_FIRST = 0x1;
+
+ /**
+ * The adapter key for the skip to previous control.
+ */
+ public static final int ACTION_SKIP_TO_PREVIOUS = 0x10;
+
+ /**
+ * The adapter key for the rewind control.
+ */
+ public static final int ACTION_REWIND = 0x20;
+
+ /**
+ * The adapter key for the play/pause control.
+ */
+ public static final int ACTION_PLAY_PAUSE = 0x40;
+
+ /**
+ * The adapter key for the fast forward control.
+ */
+ public static final int ACTION_FAST_FORWARD = 0x80;
+
+ /**
+ * The adapter key for the skip to next control.
+ */
+ public static final int ACTION_SKIP_TO_NEXT = 0x100;
+
+ /**
+ * The adapter key for the repeat control.
+ */
+ public static final int ACTION_REPEAT = 0x200;
+
+ /**
+ * The adapter key for the shuffle control.
+ */
+ public static final int ACTION_SHUFFLE = 0x400;
+
+ /**
+ * The adapter key for the first custom control on the right side
+ * of the predefined primary controls.
+ */
+ public static final int ACTION_CUSTOM_RIGHT_FIRST = 0x1000;
+
static final String TAG = "PlaybackTransportGlue";
static final boolean DEBUG = false;
@@ -148,6 +195,11 @@
mPlayerCallback.onBufferingStateChanged(start);
}
}
+
+ @Override
+ public void onMetadataChanged(PlayerAdapter wrapper) {
+ PlaybackBaseControlGlue.this.onMetadataChanged();
+ }
};
/**
@@ -341,6 +393,16 @@
mPlayerAdapter.pause();
}
+ @Override
+ public void next() {
+ mPlayerAdapter.next();
+ }
+
+ @Override
+ public void previous() {
+ mPlayerAdapter.previous();
+ }
+
protected static void notifyItemChanged(ArrayObjectAdapter adapter, Object object) {
int index = adapter.indexOf(object);
if (index >= 0) {
@@ -495,7 +557,7 @@
/**
* Event when metadata changed
*/
- void onMetadataChanged() {
+ protected void onMetadataChanged() {
if (mControlsRow == null) {
return;
}
@@ -503,7 +565,7 @@
if (DEBUG) Log.v(TAG, "updateRowMetadata");
mControlsRow.setImageDrawable(getArt());
- mControlsRow.setDuration(mPlayerAdapter.getDuration());
+ mControlsRow.setDuration(getDuration());
mControlsRow.setCurrentPosition(getCurrentPosition());
if (getHost() != null) {
@@ -545,4 +607,10 @@
mPlayerAdapter.seekTo(position);
}
+ /**
+ * Returns a bitmask of actions supported by the media player.
+ */
+ public long getSupportedActions() {
+ return mPlayerAdapter.getSupportedActions();
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java b/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
index ea09d04..983b4a9 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
@@ -16,6 +16,12 @@
package android.support.v17.leanback.media;
+import static android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction.INDEX_OFF;
+import static android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction.INDEX_ON;
+import static android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction.INDEX_NONE;
+import static android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction.INDEX_ALL;
+import static android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction.INDEX_ONE;
+
/**
* Base class that wraps underlying media player. The class is used by PlaybackGlue, for example
* {@link PlaybackTransportControlGlue} is bound to a PlayerAdapter.
@@ -96,6 +102,13 @@
*/
public void onBufferingStateChanged(PlayerAdapter adapter, boolean start) {
}
+
+ /**
+ * Event for meta data changed.
+ * @param adapter The adapter that finishes current media item.
+ */
+ public void onMetadataChanged(PlayerAdapter adapter) {
+ }
}
Callback mCallback;
@@ -134,6 +147,38 @@
public abstract void pause();
/**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_SKIP_TO_NEXT} to skip
+ * to next item.
+ */
+ public void next() {
+ }
+
+ /**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_SKIP_TO_PREVIOUS} to skip
+ * to previous item.
+ */
+ public void previous() {
+ }
+
+ /**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_FAST_FORWARD} to fast
+ * forward current media item.
+ */
+ public void fastForward() {
+ }
+
+ /**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_REWIND} to rewind in
+ * current media item.
+ */
+ public void rewind() {
+ }
+
+ /**
* Seek to new position.
* @param positionInMs New position in milliseconds.
*/
@@ -148,6 +193,29 @@
}
/**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_SHUFFLE} to set the shuffle action.
+ *
+ * @param shuffleActionIndex The repeat action. Must be one of the followings:
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction#INDEX_OFF}
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction#INDEX_ON}
+ */
+ public void setShuffleAction(int shuffleActionIndex) {
+ }
+
+ /**
+ * Optional method. Override this method if {@link #getSupportedActions()} include
+ * {@link PlaybackBaseControlGlue#ACTION_REPEAT} to set the repeat action.
+ *
+ * @param repeatActionIndex The shuffle action. Must be one of the followings:
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction#INDEX_ONE}
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction#INDEX_ALL},
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction#INDEX_NONE},
+ */
+ public void setRepeatAction(int repeatActionIndex) {
+ }
+
+ /**
* Returns true if media is currently playing.
*/
public boolean isPlaying() {
@@ -162,6 +230,14 @@
}
/**
+ * Return xor combination of values defined in PlaybackBaseControlGlue.
+ * Default is PLAY_PAUSE (unless subclass enforce to be 0)
+ */
+ public long getSupportedActions() {
+ return PlaybackBaseControlGlue.ACTION_PLAY_PAUSE;
+ }
+
+ /**
* Returns the current position of the media item in milliseconds.
*/
public long getCurrentPosition() {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index f2dae95..c58ae6e 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -2771,8 +2771,10 @@
int pos = mFocusPosition + mFocusPositionOffset;
if (positionStart <= pos) {
if (positionStart + itemCount > pos) {
- // the focus item was removed
+ // stop updating offset after the focus item was removed
mFocusPositionOffset += positionStart - pos;
+ mFocusPosition += mFocusPositionOffset;
+ mFocusPositionOffset = Integer.MIN_VALUE;
} else {
mFocusPositionOffset -= itemCount;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
index 8f0c66e..53b27c6 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
@@ -13,6 +13,7 @@
*/
package android.support.v17.leanback.widget;
+import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
@@ -122,12 +123,15 @@
public static final int CARD_TYPE_FLAG_ICON_RIGHT = 4;
public static final int CARD_TYPE_FLAG_ICON_LEFT = 8;
+ private static final String ALPHA = "alpha";
+
private ImageView mImageView;
private ViewGroup mInfoArea;
private TextView mTitleView;
private TextView mContentView;
private ImageView mBadgeImage;
private boolean mAttachedToWindow;
+ ObjectAnimator mFadeInAnimator;
/**
* Create an ImageCardView using a given theme for customization.
@@ -179,6 +183,10 @@
if (mImageView.getDrawable() == null) {
mImageView.setVisibility(View.INVISIBLE);
}
+ // Set Object Animator for image view.
+ mFadeInAnimator = ObjectAnimator.ofFloat(mImageView, ALPHA, 1f);
+ mFadeInAnimator.setDuration(
+ mImageView.getResources().getInteger(android.R.integer.config_shortAnimTime));
mInfoArea = findViewById(R.id.info_field);
if (hasImageOnly) {
@@ -324,7 +332,7 @@
mImageView.setImageDrawable(drawable);
if (drawable == null) {
- mImageView.animate().cancel();
+ mFadeInAnimator.cancel();
mImageView.setAlpha(1f);
mImageView.setVisibility(View.INVISIBLE);
} else {
@@ -332,7 +340,7 @@
if (fade) {
fadeIn();
} else {
- mImageView.animate().cancel();
+ mFadeInAnimator.cancel();
mImageView.setAlpha(1f);
}
}
@@ -458,8 +466,7 @@
private void fadeIn() {
mImageView.setAlpha(0f);
if (mAttachedToWindow) {
- mImageView.animate().alpha(1f).setDuration(
- mImageView.getResources().getInteger(android.R.integer.config_shortAnimTime));
+ mFadeInAnimator.start();
}
}
@@ -480,9 +487,8 @@
@Override
protected void onDetachedFromWindow() {
mAttachedToWindow = false;
- mImageView.animate().cancel();
+ mFadeInAnimator.cancel();
mImageView.setAlpha(1f);
super.onDetachedFromWindow();
}
-
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
new file mode 100644
index 0000000..1c0013a
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
@@ -0,0 +1,1003 @@
+/*
+ * 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 android.support.v17.leanback.media;
+
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test {@link MediaControllerAdapter}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerAdapterTest {
+
+ // SESSION_TAG
+ private static final String SESSION_TAG = "test-session";
+ private final Object mWaitLock = new Object();
+ private MediaSessionCompat mSession;
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+ private MediaSessionCallback mCallback = new MediaSessionCallback();
+ private MediaControllerCompat mControllerCompat;
+ private MediaControllerAdapter mMediaControllerAdapter;
+ private PlayerAdapterCallback mPlayerAdapterCallback;
+
+ // Instrumented context for testing purpose
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSessionCompat(getContext(), SESSION_TAG);
+ mSession.setCallback(mCallback, mHandler);
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+ mControllerCompat = new MediaControllerCompat(mContext, mSession);
+ mMediaControllerAdapter = new MediaControllerAdapter(mControllerCompat);
+ mPlayerAdapterCallback = new PlayerAdapterCallback();
+ mMediaControllerAdapter.setCallback(mPlayerAdapterCallback);
+ }
+ });
+ }
+
+ /**
+ * Check if STATE_STOPPED is associated with onPlayComplete() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStateStopped() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_STOPPED));
+ assertTrue(mPlayerAdapterCallback.mOnPlayCompletedCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_PAUSED is associated with onPlaybackStateChanged() and
+ * onCurrentPositionChanged() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStatePaused() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_PAUSED));
+ assertTrue(mPlayerAdapterCallback.mOnPlayStateChangedCalled);
+ assertTrue(mPlayerAdapterCallback.mOnCurrentPositionChangedCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_PLAYING is associated with onPlaybackStateChanged() and
+ * onCurrentPositionChanged() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStatePlaying() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_PLAYING));
+ assertTrue(mPlayerAdapterCallback.mOnPlayStateChangedCalled);
+ assertTrue(mPlayerAdapterCallback.mOnCurrentPositionChangedCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_BUFFERING is associated with onBufferingStateChanged() and
+ * onBufferedPositionChanged() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStateBuffering() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_BUFFERING));
+ assertTrue(mPlayerAdapterCallback.mOnBufferingStateChangedCalled);
+ assertTrue(mPlayerAdapterCallback.mOnBufferedPositionChangedCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_ERROR is associated with onError() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStateError() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_ERROR));
+ assertTrue(mPlayerAdapterCallback.mOnErrorCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_FAST_FORWARDING is associated with onPlaybackStateChanged() and
+ * onCurrentPositionChanged() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStateFastForwarding() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_FAST_FORWARDING));
+ assertTrue(mPlayerAdapterCallback.mOnPlayStateChangedCalled);
+ assertTrue(mPlayerAdapterCallback.mOnCurrentPositionChangedCalled);
+ }
+ }
+
+ /**
+ * Check if STATE_REWIND is associated with onPlaybackStateChanged() and
+ * onCurrentPositionChanged() callback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testStateRewinding() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onPlaybackStateChanged(
+ createPlaybackStateForTesting(PlaybackStateCompat.STATE_REWINDING));
+ assertTrue(mPlayerAdapterCallback.mOnPlayStateChangedCalled);
+ assertTrue(mPlayerAdapterCallback.mOnCurrentPositionChangedCalled);
+ }
+ }
+
+ /**
+ * Check onMetadataChanged() function in PlayerAdapterCallback.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testOnMetadataChanged() {
+ synchronized (mWaitLock) {
+ mPlayerAdapterCallback.reset();
+ mMediaControllerAdapter.mMediaControllerCallback.onMetadataChanged(
+ new MediaMetadataCompat.Builder().build());
+ assertTrue(mPlayerAdapterCallback.mOnMetadataChangedCalled);
+ }
+ }
+
+ /**
+ * Check adapter's play operation.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testPlay() throws InterruptedException {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.play();
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnPlayCalled);
+ }
+ }
+
+ /**
+ * Check adapter's pause operation.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testPause() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.pause();
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnPauseCalled);
+ }
+ }
+
+ /**
+ * Check adapter's seekTo operation.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testSeekTo() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ long seekPosition = 445L;
+ mMediaControllerAdapter.seekTo(seekPosition);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSeekToCalled);
+ assertEquals(seekPosition, mCallback.mSeekPosition);
+ }
+ }
+
+ /**
+ * Check adapter's next operation.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testNext() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.next();
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSkipToNextCalled);
+ }
+ }
+
+ /**
+ * Check adapter's previous operation.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testPrevious() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.previous();
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSkipToPreviousCalled);
+ }
+ }
+
+ /**
+ * Check adapter's setRepeatAction operation.
+ * In this test case, the repeat mode is set to REPEAT_MODE_NONE.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testRepeatModeRepeatModeNone() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(mCallback.mRepeatMode, PlaybackStateCompat.REPEAT_MODE_NONE);
+ }
+ }
+
+ /**
+ * Check adapter's setRepeatAction operation.
+ * In this test case, the repeat mode is set to REPEAT_MODE_ONE.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testRepeatModeRepeatModeOne() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_ONE);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(mCallback.mRepeatMode, PlaybackStateCompat.REPEAT_MODE_ONE);
+ }
+ }
+
+ /**
+ * Check adapter's setRepeatAction operation.
+ * In this test case, the repeat mode is set to REPEAT_MODE_ALL.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testRepeatModeRepeatModeAll() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_ALL);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(mCallback.mRepeatMode, PlaybackStateCompat.REPEAT_MODE_ALL);
+ }
+ }
+
+ /**
+ * Check adapter's setShuffleAction operation.
+ * In this test case, the shuffle mode is set to SHUFFLE_MODE_NONE.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testShuffleModeShuffleModeNone() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.setShuffleAction(PlaybackControlsRow.ShuffleAction.INDEX_OFF);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSetShuffleModeCalled);
+ assertEquals(mCallback.mShuffleMode, PlaybackStateCompat.SHUFFLE_MODE_NONE);
+ }
+ }
+
+ /**
+ * Check adapter's setShuffleAction operation.
+ * In this test case, the shuffle mode is set to SHUFFLE_MODE_ALL.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testShuffleModeShuffleModeAll() throws InterruptedException {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ mMediaControllerAdapter.setShuffleAction(PlaybackControlsRow.ShuffleAction.INDEX_ON);
+ mWaitLock.wait();
+ assertTrue(mCallback.mOnSetShuffleModeCalled);
+ assertEquals(mCallback.mShuffleMode, PlaybackStateCompat.SHUFFLE_MODE_ALL);
+ }
+ }
+
+ /**
+ * Check adapter's isPlaying operation when the playState is null.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testIsPlayingWithNullPlaybackState() {
+ boolean defualtIsPlayingStatus = false;
+ assertEquals(mMediaControllerAdapter.isPlaying(), defualtIsPlayingStatus);
+ }
+
+ /**
+ * Check adapter's isPlaying operation when the playback state is not null and the
+ * media is playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testIsPlayingWithPlayingState() {
+ long positionForTest = 0L;
+ boolean playingStatus = true;
+ createPlaybackStatePlaying(positionForTest);
+ assertEquals(mMediaControllerAdapter.isPlaying(), playingStatus);
+ }
+
+ /**
+ * Check adapter's isPlaying operation when the playback state is not null and the
+ * media is not playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testIsPlayingWithNotPlayingState() {
+ long positionForTest = 0L;
+ boolean notPlayingStatus = false;
+ createPlaybackStateNotPlaying(positionForTest);
+ assertEquals(mMediaControllerAdapter.isPlaying(), notPlayingStatus);
+ }
+
+ /**
+ * Check adapter's getCurrentPosition operation when the playbackState is null.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetCurrentPositionWithNullPlaybackState() {
+ long defaultPosition = 0L;
+ assertEquals(mMediaControllerAdapter.getCurrentPosition(), defaultPosition);
+ }
+
+ /**
+ * Check adapter's getCurrentPosition operation when the playback state is not null and the
+ * media is playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetCurrentPositionWithPlayingState() {
+
+ long positionForTest = 445L;
+ createPlaybackStatePlaying(positionForTest);
+ assertTrue(mMediaControllerAdapter.getCurrentPosition() >= positionForTest);
+ }
+
+ /**
+ * Check adapter's getBufferedPosition method when the playback state is not null and the
+ * media is not playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetCurrentPositionWithNotPlayingState() {
+
+ long positionForTest = 445L;
+ createPlaybackStateNotPlaying(positionForTest);
+ assertTrue(mMediaControllerAdapter.getCurrentPosition() == positionForTest);
+ }
+
+ /**
+ * Check adapter's getBufferedPosition method when the playback state is null.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetBufferedPositionWithNullPlaybackState() {
+ // TODO: considering chaning default buffered position to -1
+ long defaultBufferedPosition = 0L;
+ assertEquals(mMediaControllerAdapter.getBufferedPosition(), defaultBufferedPosition);
+ }
+
+ /**
+ * Check adapter's getBufferedPosition method when the playback state is not null and the
+ * media is playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetBufferedPositionWithPlayingState() {
+ long positionForTest = 445L;
+ createPlaybackStatePlayingWithBufferedPosition(positionForTest, positionForTest);
+ assertEquals(mMediaControllerAdapter.getBufferedPosition(), positionForTest);
+ }
+
+ /**
+ * Check adapter's getBufferedPosition method when the playback state is not null and the
+ * media is not playing.
+ *
+ * @throws InterruptedException wait() operation may cause InterruptedException.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetBufferedPositionWithNotPlayingState() {
+ long positionForTest = 445L;
+ createPlaybackStateNotPlayingWithBufferedPosition(positionForTest, positionForTest);
+ assertEquals(mMediaControllerAdapter.getBufferedPosition(), positionForTest);
+ }
+
+ /**
+ * check adapter's getMediaTitle() operation when the media meta data is not null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaTitleWithValidMetaData() {
+ int widthForTest = 1;
+ int heightForTest = 1;
+ Bitmap bitmapForTesting = Bitmap.createBitmap(widthForTest, heightForTest,
+ Bitmap.Config.ARGB_8888);
+ Long durationForTeting = 0L;
+ String mediaTitle = "media title";
+ String albumName = "album name";
+ String artistName = "artist name";
+
+ createMediaMetaData(bitmapForTesting, durationForTeting, mediaTitle, albumName, artistName);
+ assertEquals(mMediaControllerAdapter.getMediaTitle(), mediaTitle);
+ }
+
+ /**
+ * check adapter's getMediaTitle() operation when the media meta data is null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaTitleWithNullMetaData() {
+ String defaultMediaTitle = "";
+ assertEquals(mMediaControllerAdapter.getMediaTitle(), defaultMediaTitle);
+ }
+
+ /**
+ * check adapter's getMediaSubtitle() operation when the media meta data is not null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaSubTitleWithValidMetaData() {
+ int widthForTest = 1;
+ int heightForTest = 1;
+ Bitmap bitmapForTesting = Bitmap.createBitmap(widthForTest, heightForTest,
+ Bitmap.Config.ARGB_8888);
+ Long durationForTeting = 0L;
+ String mediaTitle = "media title";
+ String albumName = "album name";
+ String artistName = "artist name";
+
+ createMediaMetaData(bitmapForTesting, durationForTeting, mediaTitle, albumName, artistName);
+ assertEquals(mMediaControllerAdapter.getMediaSubtitle(), albumName);
+ }
+
+ /**
+ * check adapter's getMediaSubtitle() operation when the media meta data is null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaSubTitleWithNullMetaData() {
+ String defaultMediaSubTitle = "";
+ assertEquals(mMediaControllerAdapter.getMediaSubtitle(), defaultMediaSubTitle);
+ }
+
+ /**
+ * check adapter's getMediaArt operation when the media meta data is not null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaArtWithValidMetaData() {
+ int widthForTest = 1;
+ int heightForTest = 1;
+ Bitmap bitmapForTesting = Bitmap.createBitmap(widthForTest, heightForTest,
+ Bitmap.Config.ARGB_8888);
+ Long durationForTeting = 0L;
+ String mediaTitle = "media title";
+ String albumName = "album name";
+ String artistName = "artist name";
+
+ createMediaMetaData(bitmapForTesting, durationForTeting, mediaTitle, albumName, artistName);
+ Drawable testDrawable = new BitmapDrawable(mContext.getResources(), bitmapForTesting);
+ // compare two drawable objects through serveral selected fields.
+ assertEquals(mMediaControllerAdapter.getMediaArt(mContext).getBounds(),
+ testDrawable.getBounds());
+ assertEquals(mMediaControllerAdapter.getMediaArt(mContext).getAlpha(),
+ testDrawable.getAlpha());
+ assertEquals(mMediaControllerAdapter.getMediaArt(mContext).getColorFilter(),
+ testDrawable.getColorFilter());
+ }
+
+ /**
+ * check adapter's getMediaArt operation when the media meta data is null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetMediaArtWithNullMetaData() {
+ Bitmap defaultMediaArt = null;
+ assertEquals(mMediaControllerAdapter.getMediaArt(mContext), defaultMediaArt);
+ }
+
+ /**
+ * check adapter's getDuration operation when the media meta data is not null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetDurationWithValidMetaData() {
+ int widthForTest = 1;
+ int heightForTest = 1;
+ Bitmap bitmapForTesting = Bitmap.createBitmap(widthForTest, heightForTest,
+ Bitmap.Config.ARGB_8888);
+ Long durationForTeting = 45L;
+ String mediaTitle = "media title";
+ String albumName = "album name";
+ String artistName = "artist name";
+
+ createMediaMetaData(bitmapForTesting, durationForTeting, mediaTitle, albumName, artistName);
+ assertEquals((Long) mMediaControllerAdapter.getDuration(), durationForTeting);
+ }
+
+ /**
+ * check adapter's getDuration operation when the media meta data is null.
+ */
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void testGetDurationWithNullMetaData() {
+ Long defaultDuration = 0L;
+ assertEquals((Long) mMediaControllerAdapter.getDuration(), defaultDuration);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSession.release();
+ }
+
+ /**
+ * Helper function to create playback state in the situation where media is playing.
+ *
+ * @param position current media item's playing position.
+ */
+ private void createPlaybackStatePlaying(long position) {
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ int playState = PlaybackStateCompat.STATE_PLAYING;
+ long currentPosition = position;
+ playbackStateBuilder.setState(playState, currentPosition, (float) 1.0).setActions(
+ getPlaybackStateActions()
+ );
+ mSession.setPlaybackState(playbackStateBuilder.build());
+ }
+
+ /**
+ * Helper function to create playback state in the situation where media is paused.
+ *
+ * @param position current media item's playing position.
+ */
+ private void createPlaybackStateNotPlaying(long position) {
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ int playState = PlaybackStateCompat.STATE_PAUSED;
+ long currentPosition = position;
+ playbackStateBuilder.setState(playState, currentPosition, (float) 1.0).setActions(
+ getPlaybackStateActions()
+ );
+ mSession.setPlaybackState(playbackStateBuilder.build());
+ }
+
+ /**
+ * Helper function to create playback state in the situation that media is playing.
+ * Also the buffered position will be assigned in this function.
+ *
+ * @param position current media item's playing position.
+ * @param bufferedPosition current media item's buffered position.
+ */
+ private void createPlaybackStatePlayingWithBufferedPosition(long position,
+ long bufferedPosition) {
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ int playState = PlaybackStateCompat.STATE_PLAYING;
+ long currentPosition = position;
+ playbackStateBuilder.setState(playState, currentPosition, (float) 1.0).setActions(
+ getPlaybackStateActions()
+ );
+ playbackStateBuilder.setBufferedPosition(bufferedPosition);
+ mSession.setPlaybackState(playbackStateBuilder.build());
+ }
+
+ /**
+ * Helper function to create playback state in the situation that media is paused.
+ * Also the buffered position will be assigned in this function.
+ *
+ * @param position current media item's playing position.
+ * @param bufferedPosition current media item's buffered position.
+ */
+ private void createPlaybackStateNotPlayingWithBufferedPosition(long position,
+ long bufferedPosition) {
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ int playState = PlaybackStateCompat.STATE_PLAYING;
+ long currentPosition = position;
+ playbackStateBuilder.setState(playState, currentPosition, (float) 1.0).setActions(
+ getPlaybackStateActions()
+ );
+ playbackStateBuilder.setBufferedPosition(bufferedPosition);
+ mSession.setPlaybackState(playbackStateBuilder.build());
+ }
+
+ /**
+ * Helper function to compute the supported playback action.
+ *
+ * @return supported playback actions.
+ */
+ private long getPlaybackStateActions() {
+ long res = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED;
+ return res;
+ }
+
+ /**
+ * Helper function to create fake media meta data.
+ *
+ * @param bitmapForTesting Bitmap
+ * @param duratoinForTesting Duration
+ * @param mediaTitle Title.
+ * @param albumName Album name.
+ * @param artistName Artist name.
+ */
+ private void createMediaMetaData(Bitmap bitmapForTesting, Long duratoinForTesting,
+ String mediaTitle, String albumName, String artistName) {
+ MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder();
+ metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duratoinForTesting);
+ metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,
+ mediaTitle);
+ metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM,
+ albumName);
+ metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST,
+ albumName);
+ metaDataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
+ bitmapForTesting);
+ metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+ duratoinForTesting);
+ mSession.setMetadata(metaDataBuilder.build());
+ }
+
+ // helper function to create dummy playback state for testing.
+ PlaybackStateCompat createPlaybackStateForTesting(int playbackStateCompat) {
+ long currentPosition = 0L;
+ float playbackSpeed = 0.0f;
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ builder.setState(playbackStateCompat, currentPosition, playbackSpeed);
+ return builder.build();
+ }
+
+ /**
+ * Simulated MediaSessionCallback class for verification.
+ */
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private long mSeekPosition;
+ private int mRepeatMode;
+ private boolean mShuffleModeEnabled;
+ private int mShuffleMode;
+
+ private boolean mOnPlayCalled;
+ private boolean mOnPauseCalled;
+ private boolean mOnStopCalled;
+ private boolean mOnFastForwardCalled;
+ private boolean mOnRewindCalled;
+ private boolean mOnSkipToPreviousCalled;
+ private boolean mOnSkipToNextCalled;
+ private boolean mOnSeekToCalled;
+ private boolean mOnSetRepeatModeCalled;
+ private boolean mOnSetShuffleModeCalled;
+
+ public void reset() {
+ mSeekPosition = -1;
+ mShuffleModeEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+
+ mOnPlayCalled = false;
+ mOnPauseCalled = false;
+ mOnStopCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSkipToPreviousCalled = false;
+ mOnSkipToNextCalled = false;
+ mOnSeekToCalled = false;
+ mOnSetRepeatModeCalled = false;
+ mOnSetShuffleModeCalled = false;
+ }
+
+ @Override
+ public void onPlay() {
+ synchronized (mWaitLock) {
+ mOnPlayCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ synchronized (mWaitLock) {
+ mOnPauseCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (mWaitLock) {
+ mOnStopCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ synchronized (mWaitLock) {
+ mOnFastForwardCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ synchronized (mWaitLock) {
+ mOnRewindCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ synchronized (mWaitLock) {
+ mOnSkipToPreviousCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ synchronized (mWaitLock) {
+ mOnSkipToNextCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ synchronized (mWaitLock) {
+ mOnSeekToCalled = true;
+ mSeekPosition = pos;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ synchronized (mWaitLock) {
+ mOnSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class PlayerAdapterCallback extends PlayerAdapter.Callback {
+ private boolean mOnPlayStateChangedCalled;
+ private boolean mOnPreparedStateChangedCalled;
+ private boolean mOnPlayCompletedCalled;
+ private boolean mOnCurrentPositionChangedCalled;
+ private boolean mOnBufferedPositionChangedCalled;
+ private boolean mOnDurationChnagedCalled;
+ private boolean mOnVideoSizeChangedCalled;
+ private boolean mOnErrorCalled;
+ private boolean mOnBufferingStateChangedCalled;
+ private boolean mOnMetadataChangedCalled;
+
+ public void reset() {
+ mOnPlayStateChangedCalled = false;
+ mOnPreparedStateChangedCalled = false;
+ mOnPlayCompletedCalled = false;
+ mOnCurrentPositionChangedCalled = false;
+ mOnBufferedPositionChangedCalled = false;
+ mOnDurationChnagedCalled = false;
+ mOnVideoSizeChangedCalled = false;
+ mOnErrorCalled = false;
+ mOnBufferingStateChangedCalled = false;
+ mOnMetadataChangedCalled = false;
+ }
+
+ @Override
+ public void onPlayStateChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnPlayStateChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onPreparedStateChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnPreparedStateChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onPlayCompleted(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnPlayCompletedCalled = true;
+ }
+ }
+
+ @Override
+ public void onCurrentPositionChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnCurrentPositionChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onBufferedPositionChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnBufferedPositionChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onDurationChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnDurationChnagedCalled = true;
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(PlayerAdapter adapter, int width, int height) {
+ synchronized (mWaitLock) {
+ mOnVideoSizeChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onError(PlayerAdapter adapter, int errorCode, String errorMessage) {
+ synchronized (mWaitLock) {
+ mOnErrorCalled = true;
+ }
+ }
+
+ @Override
+ public void onBufferingStateChanged(PlayerAdapter adapter, boolean start) {
+ synchronized (mWaitLock) {
+ mOnBufferingStateChangedCalled = true;
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(PlayerAdapter adapter) {
+ synchronized (mWaitLock) {
+ mOnMetadataChangedCalled = true;
+ }
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 61974cc..a05e9c0 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -4532,6 +4532,68 @@
assertEquals(-1, mGridView.getSelectedPosition());
}
+ @Test
+ public void testFocusedPositonAfterRemoved1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ final int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ setSelectedPosition(1);
+ assertEquals(1, mGridView.getSelectedPosition());
+
+ final int[] newItems = new int[3];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = 300;
+ }
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 2, true);
+ mActivity.addItems(0, newItems, true);
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testFocusedPositonAfterRemoved2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ final int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ setSelectedPosition(1);
+ assertEquals(1, mGridView.getSelectedPosition());
+
+ final int[] newItems = new int[3];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = 300;
+ }
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(1, 1, true);
+ mActivity.addItems(1, newItems, true);
+ }
+ });
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
static void assertNoCollectionItemInfo(AccessibilityNodeInfoCompat info) {
AccessibilityNodeInfoCompat.CollectionItemInfoCompat nodeInfoCompat =
info.getCollectionItemInfo();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
new file mode 100644
index 0000000..a407e28
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
@@ -0,0 +1,516 @@
+/*
+ * 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 android.support.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.app.TestActivity;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.View;
+import android.widget.ImageView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ImageCardViewTest {
+
+ private static final String IMAGE_CARD_VIEW_ACTIVITY = "ImageCardViewActivity";
+ private static final float FINAL_ALPHA_STATE = 1.0f;
+ private static final float INITIAL_ALPHA_STATE = 0.0f;
+ private static final float DELTA = 0.0f;
+ private static final long ANIMATION_DURATION = 5000;
+ private static final int RANDOM_COLOR_ONE = 0xffffffff;
+ private static final int RANDOM_COLOR_TWO = 0x00000000;
+
+ @Rule
+ public TestName mUnitTestName = new TestName();
+
+ // Enable lifecycle based testing
+ private TestActivity.TestActivityTestRule mRule;
+
+ // Only support alpha animation
+ private static final String ALPHA = "alpha";
+
+ // Flag to represent if the callback has been called or not
+ private boolean mOnAnimationStartCalled;
+ private boolean mOnAnimationPauseCalled;
+ private boolean mOnAnimationResumeCalled;
+ private boolean mOnAnimationCancelCalled;
+ private boolean mOnAnimationEndCalled;
+ private boolean mOnAnimationRepeatCalled;
+
+ // ImageCardView for testing.
+ private ImageCardView mImageCardView;
+
+ // Animator for testing.
+ private ObjectAnimator mFadeInAnimator;
+
+ // ImageView on ImageCardView;
+ private ImageView mImageView;
+
+ // Sample Drawable which will be used as the parameter for some methods.
+ private Drawable mSampleDrawable;
+
+ // Another Sample Drawable.
+ private Drawable mSampleDrawable2;
+
+ // Generated Image View Id.
+ private int mImageCardViewId;
+
+ // Listener to capture animator's state
+ private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mOnAnimationStartCalled = true;
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ super.onAnimationPause(animation);
+ mOnAnimationPauseCalled = true;
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ super.onAnimationResume(animation);
+ mOnAnimationResumeCalled = true; }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ mOnAnimationCancelCalled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mOnAnimationEndCalled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ super.onAnimationRepeat(animation);
+ mOnAnimationRepeatCalled = true;
+ }
+ };
+
+ // Set up before executing test cases.
+ @Before
+ public void setUp() throws Exception {
+ // The following provider will create an Activity which can inflate the ImageCardView
+ // And the ImageCardView can be fetched through ID for future testing.
+ TestActivity.Provider imageCardViewProvider = new TestActivity.Provider() {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+
+ // The theme must be set to make sure imageCardView can be populated correctly.
+ activity.setTheme(R.style.Widget_Leanback_ImageCardView_BadgeStyle);
+
+ // Create Drawable using random color for test purpose.
+ mSampleDrawable = new ColorDrawable(RANDOM_COLOR_ONE);
+
+ // Create Drawable using random color for test purpose.
+ mSampleDrawable2 = new ColorDrawable(RANDOM_COLOR_TWO);
+
+ // Create imageCardView and save system generated ID.
+ ImageCardView imageCardView = new ImageCardView(activity);
+ mImageCardViewId = imageCardView.generateViewId();
+ imageCardView.setId(mImageCardViewId);
+
+ // Set up imageCardView with activity programmatically.
+ activity.setContentView(imageCardView);
+ }
+ };
+
+ // Initialize testing rule and testing activity
+ mRule = new TestActivity.TestActivityTestRule(imageCardViewProvider, generateProviderName(
+ IMAGE_CARD_VIEW_ACTIVITY));
+ final TestActivity imageCardViewTestActivity = mRule.launchActivity();
+
+ // Create card view and image view
+ mImageCardView = (ImageCardView) imageCardViewTestActivity.findViewById(mImageCardViewId);
+ mImageView = mImageCardView.getMainImageView();
+
+ // Create animator.
+ mFadeInAnimator = mImageCardView.mFadeInAnimator;
+ mFadeInAnimator.addListener(mAnimatorListener);
+
+ // Set animation duration with longer period of time for robust testing.
+ mFadeInAnimator.setDuration(ANIMATION_DURATION);
+ }
+
+ /**
+ * Test SetMainImage method when the parameters are null and false
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageTest0() throws Throwable {
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(null, false);
+
+ // Currently, the animation hasn't started yet, the cancel event will not be
+ // triggered
+ assertFalse(mOnAnimationCancelCalled);
+
+ // The animation will not be started, check status immediately.
+ assertEquals(mImageCardView.getMainImage(), null);
+ assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method when the parameters are mSampleDrawable and false
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageTest1() throws Throwable {
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, false);
+
+ // Currently, the animation hasn't started yet, the cancel event will not be
+ // triggered
+ assertFalse(mOnAnimationCancelCalled);
+
+ // The animation will not be started, check status immediately.
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method when the parameters are null and true
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageTest2() throws Throwable {
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(null, true);
+
+ // Currently, the animation hasn't started yet, the cancel event will not be
+ // triggered
+ assertFalse(mOnAnimationCancelCalled);
+
+ // The animation will not be started, check status immediately.
+ assertEquals(mImageCardView.getMainImage(), null);
+ assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method with sample drawable object and true parameter
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageTest3() throws Throwable {
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, true);
+
+ // The fadeIn method should be triggered in this scenario
+ assertTrue(mOnAnimationStartCalled);
+
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // Set time out limitation to be 2 * ANIMATION_DURATION.
+ PollingCheck.waitFor(2 * ANIMATION_DURATION, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mOnAnimationEndCalled;
+ }
+ });
+
+ // Test if animation ended successfully through alpha value.
+ assertTrue(mOnAnimationEndCalled);
+ assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ }
+
+ /**
+ * Test SetMainImage method's behavior when the animation is already started
+ * In this test case, the parameters are set to null and false to interrupt existed animation
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageInTransitionTest0() throws Throwable {
+ // The transition duration before the interruption happens.
+ long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+ // Perform an animation firstly
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, true);
+
+ // The fadeIn method should be triggered in this scenario
+ assertTrue(mOnAnimationStartCalled);
+
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // simulate the duration of animation
+ SystemClock.sleep(durationBeforeInterruption);
+
+ // Interrupt current animation using setMainImage(Drawable, boolean) method
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // Interrupt existed animation
+ mImageCardView.setMainImage(null, false);
+
+ // Existed animation will be cancelled immediately.
+ assertTrue(mOnAnimationCancelCalled);
+
+ // New animation will not be triggered, check the status immediately
+ assertEquals(mImageCardView.getMainImage(), null);
+ assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method's behavior when the animation is already started
+ * In this test case, the parameters are set to mSampleDrawable2 and false to interrupt
+ * existed animation
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageInTransitionTest1() throws Throwable {
+ // The transition duration before the interruption happens.
+ long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+ // Perform an animation firstly
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, true);
+
+ // The fadeIn method should be triggered in this scenario
+ assertTrue(mOnAnimationStartCalled);
+
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // simulate the duration of animation
+ SystemClock.sleep(durationBeforeInterruption);
+
+ // Interrupt current animation using setMainImage(Drawable, boolean) method
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // Interrupt existed animation
+ mImageCardView.setMainImage(mSampleDrawable2, false);
+
+ // Existed animation will be cancelled immediately.
+ assertTrue(mOnAnimationCancelCalled);
+
+ // New animation will not be triggered, check the status immediately
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable2);
+ assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method's behavior when the animation is already started
+ * In this test case, the parameters are set to null and true to interrupt existed animation
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageInTransitionTest2() throws Throwable {
+ // The transition duration before the interruption happens.
+ long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+ // Perform an animation firstly
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, true);
+
+ // The fadeIn method should be triggered in this scenario
+ assertTrue(mOnAnimationStartCalled);
+
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // simulate the duration of animation
+ SystemClock.sleep(durationBeforeInterruption);
+
+ // Interrupt current animation using setMainImage(Drawable, boolean) method
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // Interrupt existed animation
+ mImageCardView.setMainImage(null, true);
+
+ // Existed animation will be cancelled immediately.
+ assertTrue(mOnAnimationCancelCalled);
+
+ // New animation will not be triggered, check the status immediately
+ assertEquals(mImageCardView.getMainImage(), null);
+ assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Test SetMainImage method's behavior when the animation is already started
+ * In this test case, the parameters are set to mSampleDrawable2 and true to interrupt
+ * existed animation
+ *
+ * @throws Throwable
+ */
+ @Test
+ public void testSetMainImageInTransitionTest3() throws Throwable {
+ // The transition duration before the interruption happens.
+ long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+ // Perform an animation firstly
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // set random alpha value initially
+ mImageView.setAlpha(generateInitialialAlphaValue());
+ mImageCardView.setMainImage(mSampleDrawable, true);
+
+ // The fadeIn method should be triggered in this scenario
+ assertTrue(mOnAnimationStartCalled);
+
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // Simulate the duration of animation
+ SystemClock.sleep(durationBeforeInterruption);
+
+ // Interrupt current animation using setMainImage(Drawable, boolean) method
+ mRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ // Interrupt existed animation
+ mImageCardView.setMainImage(mSampleDrawable2, true);
+
+ // Existed animation will not be cancelled immediately.
+ assertFalse(mOnAnimationCancelCalled);
+
+ // New animation will not be triggered, check the status immediately
+ assertEquals(mImageCardView.getMainImage(), mSampleDrawable2);
+ assertEquals(mImageView.getVisibility(), View.VISIBLE);
+ }
+ });
+
+ // Set time out limitation to be 2 * ANIMATION_DURATION.
+ PollingCheck.waitFor(2 * ANIMATION_DURATION, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mOnAnimationEndCalled;
+ }
+ });
+
+ // Test if animation ended successfully through alpha value.
+ assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+ }
+
+
+ // Helper function to register provider's name
+ private String generateProviderName(String name) {
+ return mUnitTestName.getMethodName() + "_" + name;
+ }
+
+ // generate random number as the initial alpha value
+ private float generateInitialialAlphaValue() {
+ Random generator = new Random();
+ return generator.nextFloat();
+ }
+}
+
+
diff --git a/v7/appcompat/api/27.0.0-SNAPSHOT.txt b/v7/appcompat/api/27.0.0-SNAPSHOT.txt
index 0b26bb7..54db361 100644
--- a/v7/appcompat/api/27.0.0-SNAPSHOT.txt
+++ b/v7/appcompat/api/27.0.0-SNAPSHOT.txt
@@ -312,10 +312,6 @@
ctor public deprecated NotificationCompat.Builder(android.content.Context);
}
- public static deprecated class NotificationCompat.DecoratedCustomViewStyle extends android.support.v4.app.NotificationCompat.DecoratedCustomViewStyle {
- ctor public deprecated NotificationCompat.DecoratedCustomViewStyle();
- }
-
public static deprecated class NotificationCompat.DecoratedMediaCustomViewStyle extends android.support.v4.media.app.NotificationCompat.DecoratedMediaCustomViewStyle {
ctor public deprecated NotificationCompat.DecoratedMediaCustomViewStyle();
}
diff --git a/v7/appcompat/src/android/support/v7/app/AlertController.java b/v7/appcompat/src/android/support/v7/app/AlertController.java
index 986baf3..5ff4537 100644
--- a/v7/appcompat/src/android/support/v7/app/AlertController.java
+++ b/v7/appcompat/src/android/support/v7/app/AlertController.java
@@ -501,10 +501,8 @@
// Only show the divider if we have a title.
View divider = null;
- if (mMessage != null || mListView != null || hasCustomPanel) {
- if (!hasCustomPanel) {
- divider = topPanel.findViewById(R.id.titleDividerNoCustom);
- }
+ if (mMessage != null || mListView != null) {
+ divider = topPanel.findViewById(R.id.titleDividerNoCustom);
}
if (divider != null) {
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index 6e10d9c..6cb2c54 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -235,54 +235,6 @@
}
}
-
- /**
- * Notification style for custom views that are decorated by the system.
- *
- * <p>Instead of providing a notification that is completely custom, a developer can set this
- * style and still obtain system decorations like the notification header with the expand
- * affordance and actions.
- *
- * <p>Use {@link android.app.Notification.Builder#setCustomContentView(RemoteViews)},
- * {@link android.app.Notification.Builder#setCustomBigContentView(RemoteViews)} and
- * {@link android.app.Notification.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
- * corresponding custom views to display.
- *
- * <p>To use this style with your Notification, feed it to
- * {@link android.support.v4.app.NotificationCompat.Builder#setStyle(Style)} like so:
- * <pre class="prettyprint">
- * Notification noti = new NotificationCompat.Builder()
- * .setSmallIcon(R.drawable.ic_stat_player)
- * .setLargeIcon(albumArtBitmap))
- * .setCustomContentView(contentView)
- * .setStyle(<b>new NotificationCompat.DecoratedCustomViewStyle()</b>)
- * .build();
- * </pre>
- *
- * <p>If you are using this style, consider using the corresponding styles like
- * {@link android.support.compat.R.style#TextAppearance_Compat_Notification} or
- * {@link android.support.compat.R.style#TextAppearance_Compat_Notification_Title} in
- * your custom views in order to get the correct styling on each platform version.
- *
- * @deprecated Use {@link android.support.v4.app.NotificationCompat.DecoratedCustomViewStyle}
- * and {@link android.support.compat.R.style#TextAppearance_Compat_Notification} or
- * {@link android.support.compat.R.style#TextAppearance_Compat_Notification_Title}.
- */
- @Deprecated
- public static class DecoratedCustomViewStyle extends
- android.support.v4.app.NotificationCompat.DecoratedCustomViewStyle {
-
- /**
- * @deprecated Use
- * {@link android.support.v4.app.NotificationCompat.DecoratedCustomViewStyle
- * #DecoratedCustomViewStyle()}.
- */
- @Deprecated
- public DecoratedCustomViewStyle() {
- super();
- }
- }
-
/**
* Notification style for media custom views that are decorated by the system.
*
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index f15225c..cf6fc1f 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -2626,16 +2626,17 @@
}
@Override
- public RouteInfo getSystemRouteByDescriptorId(String id) {
+ public void onSystemRouteSelectedByDescriptorId(String id) {
+ // System route is selected, do not sync the route we selected before.
+ mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED);
int providerIndex = findProviderInfo(mSystemProvider);
if (providerIndex >= 0) {
ProviderInfo provider = mProviders.get(providerIndex);
int routeIndex = provider.findRouteByDescriptorId(id);
if (routeIndex >= 0) {
- return provider.mRoutes.get(routeIndex);
+ provider.mRoutes.get(routeIndex).select();
}
}
- return null;
}
public void addRemoteControlClient(Object rcc) {
@@ -2927,16 +2928,16 @@
private void syncWithSystemProvider(int what, Object obj) {
switch (what) {
case MSG_ROUTE_ADDED:
- mSystemProvider.onSyncRouteAdded((RouteInfo)obj);
+ mSystemProvider.onSyncRouteAdded((RouteInfo) obj);
break;
case MSG_ROUTE_REMOVED:
- mSystemProvider.onSyncRouteRemoved((RouteInfo)obj);
+ mSystemProvider.onSyncRouteRemoved((RouteInfo) obj);
break;
case MSG_ROUTE_CHANGED:
- mSystemProvider.onSyncRouteChanged((RouteInfo)obj);
+ mSystemProvider.onSyncRouteChanged((RouteInfo) obj);
break;
case MSG_ROUTE_SELECTED:
- mSystemProvider.onSyncRouteSelected((RouteInfo)obj);
+ mSystemProvider.onSyncRouteSelected((RouteInfo) obj);
break;
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index d84a069..2aab6b7 100644
--- a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
@@ -97,7 +97,7 @@
* Callbacks into the media router to synchronize state with the framework media router.
*/
public interface SyncCallback {
- MediaRouter.RouteInfo getSystemRouteByDescriptorId(String id);
+ void onSystemRouteSelectedByDescriptorId(String id);
}
protected Object getDefaultRoute() {
@@ -417,11 +417,7 @@
int index = findSystemRouteRecord(routeObj);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
- MediaRouter.RouteInfo route = mSyncCallback.getSystemRouteByDescriptorId(
- record.mRouteDescriptorId);
- if (route != null) {
- route.select();
- }
+ mSyncCallback.onSystemRouteSelectedByDescriptorId(record.mRouteDescriptorId);
}
}
}
diff --git a/wear/tests/src/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java b/wear/tests/src/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
index 07eaa87..dc7a942 100644
--- a/wear/tests/src/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
+++ b/wear/tests/src/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
@@ -23,7 +23,6 @@
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
@@ -337,10 +336,12 @@
OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
actionDrawer.setOnMenuItemClickListener(mockClickListener);
// WHEN the action drawer peek view is tapped
- onView(
- allOf(
- withParent(withId(R.id.action_drawer)),
- withId(R.id.ws_drawer_view_peek_container)))
+ onView(withId(R.id.ws_drawer_view_peek_container))
+ .perform(waitForMatchingView(
+ allOf(
+ withId(R.id.ws_drawer_view_peek_container),
+ isCompletelyDisplayed()),
+ MAX_WAIT_MS))
.perform(click());
// THEN its click listener should be notified
verify(mockClickListener).onMenuItemClick(any(MenuItem.class));