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