Merge "Revert "Made unfocusable views in RecyclerView visible when using key navigation"" into nyc-support-25.2-dev
diff --git a/api/current.txt b/api/current.txt
index 24fb7c4..a2b9eea 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -135,7 +135,9 @@
   public class CustomTabsCallback {
     ctor public CustomTabsCallback();
     method public void extraCallback(java.lang.String, android.os.Bundle);
+    method public void onMessageChannelReady(android.os.Bundle);
     method public void onNavigationEvent(int, android.os.Bundle);
+    method public void onPostMessage(java.lang.String, android.os.Bundle);
     field public static final int NAVIGATION_ABORTED = 4; // 0x4
     field public static final int NAVIGATION_FAILED = 3; // 0x3
     field public static final int NAVIGATION_FINISHED = 2; // 0x2
@@ -215,10 +217,19 @@
     method protected abstract boolean mayLaunchUrl(android.support.customtabs.CustomTabsSessionToken, android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
     method protected abstract boolean newSession(android.support.customtabs.CustomTabsSessionToken);
     method public android.os.IBinder onBind(android.content.Intent);
+    method protected abstract int postMessage(android.support.customtabs.CustomTabsSessionToken, java.lang.String, android.os.Bundle);
+    method protected abstract boolean requestPostMessageChannel(android.support.customtabs.CustomTabsSessionToken, android.net.Uri);
     method protected abstract boolean updateVisuals(android.support.customtabs.CustomTabsSessionToken, android.os.Bundle);
     method protected abstract boolean warmup(long);
     field public static final java.lang.String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
     field public static final java.lang.String KEY_URL = "android.support.customtabs.otherurls.URL";
+    field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+    field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+    field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+  }
+
+  public static abstract class CustomTabsService.Result implements java.lang.annotation.Annotation {
   }
 
   public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
@@ -229,6 +240,8 @@
 
   public final class CustomTabsSession {
     method public boolean mayLaunchUrl(android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
+    method public int postMessage(java.lang.String, android.os.Bundle);
+    method public boolean requestPostMessageChannel(android.net.Uri);
     method public boolean setActionButton(android.graphics.Bitmap, java.lang.String);
     method public boolean setSecondaryToolbarViews(android.widget.RemoteViews, int[], android.app.PendingIntent);
     method public deprecated boolean setToolbarItem(int, android.graphics.Bitmap, java.lang.String);
@@ -237,6 +250,24 @@
   public class CustomTabsSessionToken {
     method public android.support.customtabs.CustomTabsCallback getCallback();
     method public static android.support.customtabs.CustomTabsSessionToken getSessionTokenFromIntent(android.content.Intent);
+    method public boolean isAssociatedWith(android.support.customtabs.CustomTabsSession);
+  }
+
+  public class PostMessageService extends android.app.Service {
+    ctor public PostMessageService();
+    method public android.os.IBinder onBind(android.content.Intent);
+  }
+
+  public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+    ctor public PostMessageServiceConnection(android.support.customtabs.CustomTabsSessionToken);
+    method public boolean bindSessionToPostMessageService(android.content.Context, java.lang.String);
+    method public final boolean notifyMessageChannelReady(android.os.Bundle);
+    method public void onPostMessageServiceConnected();
+    method public void onPostMessageServiceDisconnected();
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+    method public final void onServiceDisconnected(android.content.ComponentName);
+    method public final boolean postMessage(java.lang.String, android.os.Bundle);
+    method public void unbindFromContext(android.content.Context);
   }
 
 }
@@ -874,7 +905,8 @@
     method public java.lang.String getAttribute(java.lang.String);
     method public double getAttributeDouble(java.lang.String, double);
     method public int getAttributeInt(java.lang.String, int);
-    method public boolean getLatLong(float[]);
+    method public deprecated boolean getLatLong(float[]);
+    method public double[] getLatLong();
     method public byte[] getThumbnail();
     method public android.graphics.Bitmap getThumbnailBitmap();
     method public byte[] getThumbnailBytes();
@@ -883,6 +915,7 @@
     method public boolean isThumbnailCompressed();
     method public void saveAttributes() throws java.io.IOException;
     method public void setAttribute(java.lang.String, java.lang.String);
+    method public void setLatLong(double, double);
     field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
     field public static final int ORIENTATION_FLIP_VERTICAL = 4; // 0x4
     field public static final int ORIENTATION_NORMAL = 1; // 0x1
@@ -5971,9 +6004,11 @@
     method public java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue();
     method public java.lang.CharSequence getQueueTitle();
     method public int getRatingType();
+    method public int getRepeatMode();
     method public android.app.PendingIntent getSessionActivity();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
     method public android.support.v4.media.session.MediaControllerCompat.TransportControls getTransportControls();
+    method public boolean isShuffleModeEnabled();
     method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback);
     method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback, android.os.Handler);
     method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
@@ -5991,8 +6026,10 @@
     method public void onPlaybackStateChanged(android.support.v4.media.session.PlaybackStateCompat);
     method public void onQueueChanged(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
     method public void onQueueTitleChanged(java.lang.CharSequence);
+    method public void onRepeatModeChanged(int);
     method public void onSessionDestroyed();
     method public void onSessionEvent(java.lang.String, android.os.Bundle);
+    method public void onShuffleModeChanged(boolean);
   }
 
   public static final class MediaControllerCompat.PlaybackInfo {
@@ -6021,6 +6058,8 @@
     method public abstract void sendCustomAction(android.support.v4.media.session.PlaybackStateCompat.CustomAction, android.os.Bundle);
     method public abstract void sendCustomAction(java.lang.String, android.os.Bundle);
     method public abstract void setRating(android.support.v4.media.RatingCompat);
+    method public abstract void setRepeatMode(int);
+    method public abstract void setShuffleModeEnabled(boolean);
     method public abstract void skipToNext();
     method public abstract void skipToPrevious();
     method public abstract void skipToQueueItem(long);
@@ -6054,7 +6093,9 @@
     method public void setQueue(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
     method public void setQueueTitle(java.lang.CharSequence);
     method public void setRatingType(int);
+    method public void setRepeatMode(int);
     method public void setSessionActivity(android.app.PendingIntent);
+    method public void setShuffleModeEnabled(boolean);
     field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
     field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
   }
@@ -6077,6 +6118,8 @@
     method public void onRewind();
     method public void onSeekTo(long);
     method public void onSetRating(android.support.v4.media.RatingCompat);
+    method public void onSetRepeatMode(int);
+    method public void onSetShuffleModeEnabled(boolean);
     method public void onSkipToNext();
     method public void onSkipToPrevious();
     method public void onSkipToQueueItem(long);
@@ -6153,6 +6196,8 @@
     field public static final long ACTION_REWIND = 8L; // 0x8L
     field public static final long ACTION_SEEK_TO = 256L; // 0x100L
     field public static final long ACTION_SET_RATING = 128L; // 0x80L
+    field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
+    field public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 524288L; // 0x80000L
     field public static final long ACTION_SKIP_TO_NEXT = 32L; // 0x20L
     field public static final long ACTION_SKIP_TO_PREVIOUS = 16L; // 0x10L
     field public static final long ACTION_SKIP_TO_QUEUE_ITEM = 4096L; // 0x1000L
@@ -6171,6 +6216,9 @@
     field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; // 0x9
     field public static final int ERROR_CODE_UNKNOWN_ERROR = 0; // 0x0
     field public static final long PLAYBACK_POSITION_UNKNOWN = -1L; // 0xffffffffffffffffL
+    field public static final int REPEAT_MODE_ALL = 2; // 0x2
+    field public static final int REPEAT_MODE_NONE = 0; // 0x0
+    field public static final int REPEAT_MODE_ONE = 1; // 0x1
     field public static final int STATE_BUFFERING = 6; // 0x6
     field public static final int STATE_CONNECTING = 8; // 0x8
     field public static final int STATE_ERROR = 7; // 0x7
diff --git a/compat/java/android/support/v4/app/NotificationCompatExtras.java b/compat/java/android/support/v4/app/NotificationCompatExtras.java
index 6a2ee93..bf9ba0f 100644
--- a/compat/java/android/support/v4/app/NotificationCompatExtras.java
+++ b/compat/java/android/support/v4/app/NotificationCompatExtras.java
@@ -61,7 +61,7 @@
      * Extras key used internally by {@link NotificationCompat} to store the value of
      * the {@link android.app.Notification.Action#getRemoteInputs} before the field
      * was available.
-     * If possible, use {@link NotificationCompat.Action#getRemoteInputs to access this field.
+     * If possible, use {@link NotificationCompat.Action#getRemoteInputs} to access this field.
      */
     public static final String EXTRA_REMOTE_INPUTS =
             NotificationCompatJellybean.EXTRA_REMOTE_INPUTS;
diff --git a/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java b/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
index f01ac17..26045f7 100644
--- a/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
@@ -79,7 +79,7 @@
      * Request authentication of a crypto object. This call warms up the fingerprint hardware
      * and starts scanning for a fingerprint. It terminates when
      * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
-     * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult) is called, at
+     * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
      * provided cancel object.
      *
diff --git a/core-utils/java/android/support/v4/app/TaskStackBuilder.java b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
index 51b5f1d..0358b46 100644
--- a/core-utils/java/android/support/v4/app/TaskStackBuilder.java
+++ b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
@@ -313,7 +313,7 @@
      * the new task stack will be created in its entirety.</p>
      *
      * @param options Additional options for how the Activity should be started.
-     * See {@link android.content.Context#startActivity(Intent, Bundle)
+     * See {@link android.content.Context#startActivity(Intent, Bundle)}
      */
     public void startActivities(Bundle options) {
         if (mIntents.isEmpty()) {
@@ -357,7 +357,7 @@
      *              {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the
      *              intent that can be supplied when the actual send happens.
      * @param options Additional options for how the Activity should be started.
-     * See {@link android.content.Context#startActivity(Intent, Bundle)
+     * See {@link android.content.Context#startActivity(Intent, Bundle)}
      * @return The obtained PendingIntent
      */
     public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) {
diff --git a/customtabs/build.gradle b/customtabs/build.gradle
index e427d1e..bfbeb1a 100644
--- a/customtabs/build.gradle
+++ b/customtabs/build.gradle
@@ -31,6 +31,7 @@
 
         androidTest.setRoot('tests')
         androidTest.java.srcDir('tests/src/')
+        androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
     }
 
     compileOptions {
diff --git a/customtabs/src/android/support/customtabs/CustomTabsCallback.java b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
index d7fdd39..818118a 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsCallback.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
@@ -19,7 +19,8 @@
 import android.os.Bundle;
 
 /**
- * A callback class for custom tabs client to get messages regarding events in their custom tabs.
+ * A callback class for custom tabs client to get messages regarding events in their custom tabs. In
+ * the implementation, all callbacks are sent to the UI thread for the client.
  */
 public class CustomTabsCallback {
     /**
@@ -76,4 +77,25 @@
      * @param args Arguments for the calback
      */
     public void extraCallback(String callbackName, Bundle args) {}
+
+    /**
+     * Called when {@link CustomTabsSession} has requested a postMessage channel through
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, android.net.Uri)} and the channel
+     * is ready for sending and receiving messages on both ends.
+     *
+     * @param extras Reserved for future use.
+     */
+    public void onMessageChannelReady(Bundle extras) {}
+
+    /**
+     * Called when a tab controlled by this {@link CustomTabsSession} has sent a postMessage.
+     * If postMessage() is called from a single thread, then the messages will be posted in the
+     * same order. When received on the client side, it is the client's responsibility to preserve
+     * the ordering further.
+     *
+     * @param message The message sent.
+     * @param extras Reserved for future use.
+     */
+    public void onPostMessage(String message, Bundle extras) {}
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsClient.java b/customtabs/src/android/support/customtabs/CustomTabsClient.java
index 548c152..09f3110 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsClient.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsClient.java
@@ -26,6 +26,8 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
@@ -72,7 +74,7 @@
     /**
      * Returns the preferred package to use for Custom Tabs, preferring the default VIEW handler.
      *
-     * @see {@link #getPackageName(Context, List<String>, boolean)}.
+     * @see #getPackageName(Context, List<String>, boolean)
      */
     public static String getPackageName(Context context, @Nullable List<String> packages) {
         return getPackageName(context, packages, false);
@@ -177,21 +179,60 @@
      * then later with a Custom Tab. The client can then send later service calls or intents to
      * through same session-intent-Custom Tab association.
      * @param callback The callback through which the client will receive updates about the created
-     *                 session. Can be null.
+     *                 session. Can be null. All the callbacks will be received on the UI thread.
      * @return The session object that was created as a result of the transaction. The client can
-     *         use this to relay {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)} calls.
+     *         use this to relay session specific calls.
      *         Null on error.
      */
     public CustomTabsSession newSession(final CustomTabsCallback callback) {
         ICustomTabsCallback.Stub wrapper = new ICustomTabsCallback.Stub() {
+            private Handler mHandler = new Handler(Looper.getMainLooper());
+
             @Override
-            public void onNavigationEvent(int navigationEvent, Bundle extras) {
-                if (callback != null) callback.onNavigationEvent(navigationEvent, extras);
+            public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onNavigationEvent(navigationEvent, extras);
+                    }
+                });
             }
 
             @Override
-            public void extraCallback(String callbackName, Bundle args) throws RemoteException {
-                if (callback != null) callback.extraCallback(callbackName, args);
+            public void extraCallback(final String callbackName, final Bundle args)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.extraCallback(callbackName, args);
+                    }
+                });
+            }
+
+            @Override
+            public void onMessageChannelReady(final Bundle extras)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onMessageChannelReady(extras);
+                    }
+                });
+            }
+
+            @Override
+            public void onPostMessage(final String message, final Bundle extras)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onPostMessage(message, extras);
+                    }
+                });
             }
         };
 
diff --git a/customtabs/src/android/support/customtabs/CustomTabsIntent.java b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
index f2de314..009182a 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsIntent.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
@@ -393,8 +393,8 @@
         /**
          * Sets the action button that is displayed in the Toolbar with default tinting behavior.
          *
-         * @see {@link CustomTabsIntent.Builder#setActionButton(
-         * Bitmap, String, PendingIntent, boolean)}
+         * @see CustomTabsIntent.Builder#setActionButton(
+         * Bitmap, String, PendingIntent, boolean)
          */
         public Builder setActionButton(@NonNull Bitmap icon, @NonNull String description,
                 @NonNull PendingIntent pendingIntent) {
diff --git a/customtabs/src/android/support/customtabs/CustomTabsService.java b/customtabs/src/android/support/customtabs/CustomTabsService.java
index 25697c5..1ff1d7f 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsService.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsService.java
@@ -23,6 +23,7 @@
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.support.annotation.IntDef;
 import android.support.v4.util.ArrayMap;
 
 import java.util.List;
@@ -35,162 +36,233 @@
  * implementers that want to provide Custom Tabs functionality, not by clients that want to launch
  * Custom Tabs.
  */
- public abstract class CustomTabsService extends Service {
-     /**
-      * The Intent action that a CustomTabsService must respond to.
-      */
-     public static final String ACTION_CUSTOM_TABS_CONNECTION =
-             "android.support.customtabs.action.CustomTabsService";
+public abstract class CustomTabsService extends Service {
+    /**
+     * The Intent action that a CustomTabsService must respond to.
+     */
+    public static final String ACTION_CUSTOM_TABS_CONNECTION =
+            "android.support.customtabs.action.CustomTabsService";
 
-     /**
-      * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
-      * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
-      * to insert a new url to each bundle inside list of bundles.
-      */
-     public static final String KEY_URL =
-             "android.support.customtabs.otherurls.URL";
+    /**
+     * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
+     * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
+     * to insert a new url to each bundle inside list of bundles.
+     */
+    public static final String KEY_URL =
+            "android.support.customtabs.otherurls.URL";
 
-     private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
+    @IntDef({RESULT_SUCCESS, RESULT_FAILURE_DISALLOWED,
+            RESULT_FAILURE_REMOTE_ERROR, RESULT_FAILURE_MESSAGING_ERROR})
+    public @interface Result {
+    }
 
-     private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
+    /**
+     * Indicates that the postMessage request was accepted.
+     */
+    public static final int RESULT_SUCCESS = 0;
+    /**
+     * Indicates that the postMessage request was not allowed due to a bad argument or requesting
+     * at a disallowed time like when in background.
+     */
+    public static final int RESULT_FAILURE_DISALLOWED = -1;
+    /**
+     * Indicates that the postMessage request has failed due to a {@link RemoteException} .
+     */
+    public static final int RESULT_FAILURE_REMOTE_ERROR = -2;
+    /**
+     * Indicates that the postMessage request has failed due to an internal error on the browser
+     * message channel.
+     */
+    public static final int RESULT_FAILURE_MESSAGING_ERROR = -3;
 
-         @Override
-         public boolean warmup(long flags) {
-             return CustomTabsService.this.warmup(flags);
-         }
+    private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
 
-         @Override
-         public boolean newSession(ICustomTabsCallback callback) {
-             final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
-             try {
-                 DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
-                     @Override
-                     public void binderDied() {
-                         cleanUpSession(sessionToken);
-                     }
-                 };
-                 synchronized (mDeathRecipientMap) {
-                     callback.asBinder().linkToDeath(deathRecipient, 0);
-                     mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
-                 }
-                 return CustomTabsService.this.newSession(sessionToken);
-             } catch (RemoteException e) {
-                 return false;
-             }
-         }
+    private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
 
-         @Override
-         public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
-                         Bundle extras, List<Bundle> otherLikelyBundles) {
-             return CustomTabsService.this.mayLaunchUrl(
-                     new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
-         }
+        @Override
+        public boolean warmup(long flags) {
+            return CustomTabsService.this.warmup(flags);
+        }
 
-         @Override
-         public Bundle extraCommand(String commandName, Bundle args) {
-             return CustomTabsService.this.extraCommand(commandName, args);
-         }
+        @Override
+        public boolean newSession(ICustomTabsCallback callback) {
+            final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
+            try {
+                DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
+                    @Override
+                    public void binderDied() {
+                        cleanUpSession(sessionToken);
+                    }
+                };
+                synchronized (mDeathRecipientMap) {
+                    callback.asBinder().linkToDeath(deathRecipient, 0);
+                    mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
+                }
+                return CustomTabsService.this.newSession(sessionToken);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
 
-         @Override
-         public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
-             return CustomTabsService.this.updateVisuals(
-                     new CustomTabsSessionToken(callback), bundle);
-         }
-     };
+        @Override
+        public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
+                                    Bundle extras, List<Bundle> otherLikelyBundles) {
+            return CustomTabsService.this.mayLaunchUrl(
+                    new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
+        }
 
-     @Override
-     public IBinder onBind(Intent intent) {
-         return mBinder;
-     }
+        @Override
+        public Bundle extraCommand(String commandName, Bundle args) {
+            return CustomTabsService.this.extraCommand(commandName, args);
+        }
 
-     /**
-      * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
-      * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
-      * @param sessionToken The session token for which the {@link DeathRecipient} call has been
-      *                     received.
-      * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
-      *         same binder will return false.
-      */
-     protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
-         try {
-             synchronized (mDeathRecipientMap) {
+        @Override
+        public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
+            return CustomTabsService.this.updateVisuals(
+                    new CustomTabsSessionToken(callback), bundle);
+        }
+
+        @Override
+        public boolean requestPostMessageChannel(ICustomTabsCallback callback,
+                                                 Uri postMessageOrigin) {
+            return CustomTabsService.this.requestPostMessageChannel(
+                    new CustomTabsSessionToken(callback), postMessageOrigin);
+        }
+
+        @Override
+        public int postMessage(ICustomTabsCallback callback, String message, Bundle extras) {
+            return CustomTabsService.this.postMessage(
+                    new CustomTabsSessionToken(callback), message, extras);
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
+     * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
+     *
+     * @param sessionToken The session token for which the {@link DeathRecipient} call has been
+     *                     received.
+     * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
+     * same binder will return false.
+     */
+    protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
+        try {
+            synchronized (mDeathRecipientMap) {
                 IBinder binder = sessionToken.getCallbackBinder();
                 DeathRecipient deathRecipient =
                         mDeathRecipientMap.get(binder);
                 binder.unlinkToDeath(deathRecipient, 0);
                 mDeathRecipientMap.remove(binder);
             }
-         } catch (NoSuchElementException e) {
-             return false;
-         }
-         return true;
-     }
+        } catch (NoSuchElementException e) {
+            return false;
+        }
+        return true;
+    }
 
-     /**
-      * Warms up the browser process asynchronously.
-      *
-      * @param flags Reserved for future use.
-      * @return      Whether warmup was/had been completed successfully. Multiple successful
-      *              calls will return true.
-      */
-     protected abstract boolean warmup(long flags);
+    /**
+     * Warms up the browser process asynchronously.
+     *
+     * @param flags Reserved for future use.
+     * @return Whether warmup was/had been completed successfully. Multiple successful
+     * calls will return true.
+     */
+    protected abstract boolean warmup(long flags);
 
-     /**
-      * Creates a new session through an ICustomTabsService with the optional callback. This session
-      * can be used to associate any related communication through the service with an intent and
-      * then later with a Custom Tab. The client can then send later service calls or intents to
-      * through same session-intent-Custom Tab association.
-      * @param sessionToken Session token to be used as a unique identifier. This also has access
-      *                     to the {@link CustomTabsCallback} passed from the client side through
-      *                     {@link CustomTabsSessionToken#getCallback()}.
-      * @return             Whether a new session was successfully created.
-      */
-     protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
+    /**
+     * Creates a new session through an ICustomTabsService with the optional callback. This session
+     * can be used to associate any related communication through the service with an intent and
+     * then later with a Custom Tab. The client can then send later service calls or intents to
+     * through same session-intent-Custom Tab association.
+     *
+     * @param sessionToken Session token to be used as a unique identifier. This also has access
+     *                     to the {@link CustomTabsCallback} passed from the client side through
+     *                     {@link CustomTabsSessionToken#getCallback()}.
+     * @return Whether a new session was successfully created.
+     */
+    protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
 
-     /**
-      * Tells the browser of a likely future navigation to a URL.
-      *
-      * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
-      * The most likely URL has to be specified explicitly. Optionally, a list of
-      * other likely URLs can be provided. They are treated as less likely than
-      * the first one, and have to be sorted in decreasing priority order. These
-      * additional URLs may be ignored.
-      * All previous calls to this method will be deprioritized.
-      *
-      * @param sessionToken       The unique identifier for the session. Can not be null.
-      * @param url                Most likely URL.
-      * @param extras             Reserved for future use.
-      * @param otherLikelyBundles Other likely destinations, sorted in decreasing
-      *                           likelihood order. Each Bundle has to provide a url.
-      * @return                   Whether the call was successful.
-      */
-     protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
-             Bundle extras, List<Bundle> otherLikelyBundles);
+    /**
+     * Tells the browser of a likely future navigation to a URL.
+     * <p>
+     * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
+     * The most likely URL has to be specified explicitly. Optionally, a list of
+     * other likely URLs can be provided. They are treated as less likely than
+     * the first one, and have to be sorted in decreasing priority order. These
+     * additional URLs may be ignored.
+     * All previous calls to this method will be deprioritized.
+     *
+     * @param sessionToken       The unique identifier for the session. Can not be null.
+     * @param url                Most likely URL.
+     * @param extras             Reserved for future use.
+     * @param otherLikelyBundles Other likely destinations, sorted in decreasing
+     *                           likelihood order. Each Bundle has to provide a url.
+     * @return Whether the call was successful.
+     */
+    protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
+                                            Bundle extras, List<Bundle> otherLikelyBundles);
 
-     /**
-      * Unsupported commands that may be provided by the implementation.
-      *
-      * <p>
-      * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
-      * defined behavior, as it is entirely implementation-defined and not supported.
-      *
-      * <p> This call can be used by implementations to add extra commands, for testing or
-      * experimental purposes.
-      *
-      * @param commandName Name of the extra command to execute.
-      * @param args Arguments for the command
-      * @return The result {@link Bundle}, or null.
-      */
-     protected abstract Bundle extraCommand(String commandName, Bundle args);
+    /**
+     * Unsupported commands that may be provided by the implementation.
+     * <p>
+     * <p>
+     * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
+     * defined behavior, as it is entirely implementation-defined and not supported.
+     * <p>
+     * <p> This call can be used by implementations to add extra commands, for testing or
+     * experimental purposes.
+     *
+     * @param commandName Name of the extra command to execute.
+     * @param args        Arguments for the command
+     * @return The result {@link Bundle}, or null.
+     */
+    protected abstract Bundle extraCommand(String commandName, Bundle args);
 
     /**
      * Updates the visuals of custom tabs for the given session. Will only succeed if the given
      * session matches the currently active one.
+     *
      * @param sessionToken The currently active session that the custom tab belongs to.
      * @param bundle       The action button configuration bundle. This bundle should be constructed
      *                     with the same structure in {@link CustomTabsIntent.Builder}.
      * @return Whether the operation was successful.
      */
-     protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
-             Bundle bundle);
- }
+    protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
+                                             Bundle bundle);
+
+    /**
+     * Sends a request to create a two way postMessage channel between the client and the browser
+     * linked with the given {@link CustomTabsSession}.
+     *
+     * @param sessionToken      The unique identifier for the session. Can not be null.
+     * @param postMessageOrigin A origin that the client is requesting to be identified as
+     *                          during the postMessage communication.
+     * @return Whether the implementation accepted the request. Note that returning true
+     * here doesn't mean an origin has already been assigned as the validation is
+     * asynchronous.
+     */
+    protected abstract boolean requestPostMessageChannel(CustomTabsSessionToken sessionToken,
+                                                         Uri postMessageOrigin);
+
+    /**
+     * Sends a postMessage request using the origin communicated via
+     * {@link CustomTabsService#requestPostMessageChannel(
+     *CustomTabsSessionToken, Uri)}. Fails when called before
+     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on the
+     * client side.
+     *
+     * @param sessionToken The unique identifier for the session. Can not be null.
+     * @param message      The message that is being sent.
+     * @param extras       Reserved for future use.
+     * @return An integer constant about the postMessage request result. Will return
+     * {@link CustomTabsService#RESULT_SUCCESS} if successful.
+     */
+    @Result
+    protected abstract int postMessage(
+            CustomTabsSessionToken sessionToken, String message, Bundle extras);
+}
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSession.java b/customtabs/src/android/support/customtabs/CustomTabsSession.java
index bc05f8b..cad897c 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSession.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSession.java
@@ -25,6 +25,7 @@
 import android.os.RemoteException;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.customtabs.CustomTabsService.Result;
 import android.view.View;
 import android.widget.RemoteViews;
 
@@ -36,6 +37,7 @@
  */
 public final class CustomTabsSession {
     private static final String TAG = "CustomTabsSession";
+    private final Object mLock = new Object();
     private final ICustomTabsService mService;
     private final ICustomTabsCallback mCallback;
     private final ComponentName mComponentName;
@@ -78,7 +80,7 @@
      * @param icon          The new icon of the action button.
      * @param description   Content description of the action button.
      *
-     * @see {@link CustomTabsSession#setToolbarItem(int, Bitmap, String)}
+     * @see CustomTabsSession#setToolbarItem(int, Bitmap, String)
      */
     public boolean setActionButton(@NonNull Bitmap icon, @NonNull String description) {
         Bundle bundle = new Bundle();
@@ -142,6 +144,47 @@
         }
     }
 
+    /**
+     * Sends a request to create a two way postMessage channel between the client and the browser.
+     *
+     * @param postMessageOrigin      A origin that the client is requesting to be identified as
+     *                               during the postMessage communication.
+     * @return Whether the implementation accepted the request. Note that returning true
+     *         here doesn't mean an origin has already been assigned as the validation is
+     *         asynchronous.
+     */
+    public boolean requestPostMessageChannel(Uri postMessageOrigin) {
+        try {
+            return mService.requestPostMessageChannel(
+                    mCallback, postMessageOrigin);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Sends a postMessage request using the origin communicated via
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, Uri)}. Fails when called before
+     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on
+     * the client side.
+     *
+     * @param message The message that is being sent.
+     * @param extras Reserved for future use.
+     * @return An integer constant about the postMessage request result. Will return
+      *        {@link CustomTabsService#RESULT_SUCCESS} if successful.
+     */
+    @Result
+    public int postMessage(String message, Bundle extras) {
+        synchronized (mLock) {
+            try {
+                return mService.postMessage(mCallback, message, extras);
+            } catch (RemoteException e) {
+                return CustomTabsService.RESULT_FAILURE_REMOTE_ERROR;
+            }
+        }
+    }
+
     /* package */ IBinder getBinder() {
         return mCallback.asBinder();
     }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
index fdb5f91..adfadd9 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
@@ -58,6 +58,33 @@
                     Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
                 }
             }
+
+            @Override
+            public void extraCallback(String callbackName, Bundle args) {
+                try {
+                    mCallbackBinder.extraCallback(callbackName, args);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
+
+            @Override
+            public void onMessageChannelReady(Bundle extras) {
+                try {
+                    mCallbackBinder.onMessageChannelReady(extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
+
+            @Override
+            public void onPostMessage(String message, Bundle extras) {
+                try {
+                    mCallbackBinder.onPostMessage(message, extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
         };
     }
 
@@ -84,4 +111,11 @@
     public CustomTabsCallback getCallback() {
         return mCallback;
     }
-}
\ No newline at end of file
+
+    /**
+     * @return Whether this token is associated with the given session.
+     */
+    public boolean isAssociatedWith(CustomTabsSession session) {
+        return session.getBinder().equals(mCallbackBinder);
+    }
+}
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
index a467864..32b6e9b 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
@@ -22,7 +22,9 @@
  * Interface to a CustomTabsCallback.
  * @hide
  */
-oneway interface ICustomTabsCallback {
+interface ICustomTabsCallback {
     void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
     void extraCallback(String callbackName, in Bundle args) = 2;
+    void onMessageChannelReady(in Bundle extras) = 3;
+    void onPostMessage(String message, in Bundle extras) = 4;
 }
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
index e53ca84..b24b0dd 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
@@ -16,6 +16,7 @@
 
 package android.support.customtabs;
 
+import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.customtabs.ICustomTabsCallback;
@@ -33,4 +34,6 @@
             in Bundle extras, in List<Bundle> otherLikelyBundles) = 3;
     Bundle extraCommand(String commandName, in Bundle args) = 4;
     boolean updateVisuals(in ICustomTabsCallback callback, in Bundle bundle) = 5;
+    boolean requestPostMessageChannel(in ICustomTabsCallback callback, in Uri postMessageOrigin) = 6;
+    int postMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 7;
 }
diff --git a/customtabs/src/android/support/customtabs/IPostMessageService.aidl b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
new file mode 100644
index 0000000..2c8a605
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.customtabs.ICustomTabsCallback;
+
+/**
+ * Interface to a PostMessageService.
+ * @hide
+ */
+interface IPostMessageService {
+    void onMessageChannelReady(in ICustomTabsCallback callback, in Bundle extras) = 1;
+    void onPostMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 2;
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageService.java b/customtabs/src/android/support/customtabs/PostMessageService.java
new file mode 100644
index 0000000..7355f4e
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageService.java
@@ -0,0 +1,48 @@
+/*
+ * 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.customtabs;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A service to receive postMessage related communication from a Custom Tabs provider.
+ */
+public class PostMessageService extends Service {
+    private IPostMessageService.Stub mBinder = new IPostMessageService.Stub() {
+
+        @Override
+        public void onMessageChannelReady(
+                ICustomTabsCallback callback, Bundle extras) throws RemoteException {
+            callback.onMessageChannelReady(extras);
+        }
+
+        @Override
+        public void onPostMessage(ICustomTabsCallback callback,
+                                  String message, Bundle extras) throws RemoteException {
+            callback.onPostMessage(message, extras);
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
new file mode 100644
index 0000000..4eef50c
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
@@ -0,0 +1,126 @@
+/*
+ * 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.customtabs;
+
+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.os.RemoteException;
+
+/**
+ * A {@link ServiceConnection} for Custom Tabs providers to use while connecting to a
+ * {@link PostMessageService} on the client side.
+ */
+public abstract class PostMessageServiceConnection implements ServiceConnection {
+    private final Object mLock = new Object();
+    private final ICustomTabsCallback mSessionBinder;
+    private IPostMessageService mService;
+
+    public PostMessageServiceConnection(CustomTabsSessionToken session) {
+        mSessionBinder = ICustomTabsCallback.Stub.asInterface(session.getCallbackBinder());
+    }
+
+    /**
+     * Binds the browser side to the client app through the given {@link PostMessageService} name.
+     * After this, this {@link PostMessageServiceConnection} can be used for sending postMessage
+     * related communication back to the client.
+     * @param context A context to bind to the service.
+     * @param packageName The name of the package to be bound to.
+     * @return Whether the binding was successful.
+     */
+    public boolean bindSessionToPostMessageService(Context context, String packageName) {
+        Intent intent = new Intent();
+        intent.setClassName(packageName, PostMessageService.class.getName());
+        return context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+    }
+
+    /**
+     * Unbinds this service connection from the given context.
+     * @param context The context to be unbound from.
+     */
+    public void unbindFromContext(Context context) {
+        context.unbindService(this);
+    }
+
+    @Override
+    public final void onServiceConnected(ComponentName name, IBinder service) {
+        mService = IPostMessageService.Stub.asInterface(service);
+        onPostMessageServiceConnected();
+    }
+
+    @Override
+    public final void onServiceDisconnected(ComponentName name) {
+        mService = null;
+        onPostMessageServiceDisconnected();
+    }
+
+    /**
+     * Notifies the client that the postMessage channel requested with
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, android.net.Uri)} is ready. This method should be
+     * called when the browser binds to the client side {@link PostMessageService} and also readies
+     * a connection to the web frame.
+     *
+     * @param extras Reserved for future use.
+     * @return Whether the notification was sent to the remote successfully.
+     */
+    public final boolean notifyMessageChannelReady(Bundle extras) {
+        if (mService == null) return false;
+        synchronized (mLock) {
+            try {
+                mService.onMessageChannelReady(mSessionBinder, extras);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Posts a message to the client. This should be called when a tab controlled by related
+     * {@link CustomTabsSession} has sent a postMessage. If postMessage() is called from a single
+     * thread, then the messages will be posted in the same order.
+     *
+     * @param message The message sent.
+     * @param extras Reserved for future use.
+     * @return Whether the postMessage was sent to the remote successfully.
+     */
+    public final boolean postMessage(String message, Bundle extras) {
+        if (mService == null) return false;
+        synchronized (mLock) {
+            try {
+                mService.onPostMessage(mSessionBinder, message, extras);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Called when the {@link PostMessageService} connection is established.
+     */
+    public void onPostMessageServiceConnected() {}
+
+    /**
+     * Called when the connection is lost with the {@link PostMessageService}.
+     */
+    public void onPostMessageServiceDisconnected() {}
+}
diff --git a/customtabs/tests/src/AndroidManifest.xml b/customtabs/tests/AndroidManifest.xml
similarity index 76%
rename from customtabs/tests/src/AndroidManifest.xml
rename to customtabs/tests/AndroidManifest.xml
index d87a83f..6fe8ad9 100644
--- a/customtabs/tests/src/AndroidManifest.xml
+++ b/customtabs/tests/AndroidManifest.xml
@@ -23,9 +23,12 @@
         tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
               android.support.test.espresso, android.support.test.espresso.idling" />
 
-    <application/>
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity   android:name="android.support.customtabs.TestActivity"/>
 
-    <instrumentation
-            android:name="android.test.InstrumentationTestRunner"
-            android:targetPackage="android.support.customtabs.test"/>
+        <service    android:name="android.support.customtabs.PostMessageService"/>
+
+        <service    android:name="android.support.customtabs.TestCustomTabsService"/>
+    </application>
 </manifest>
diff --git a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
index 0fc69f9..c965361 100644
--- a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
+++ b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
@@ -39,6 +39,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CustomTabsIntentTest {
+
     @Test
     public void testBareboneCustomTabIntent() {
         CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
diff --git a/customtabs/tests/src/android/support/customtabs/PollingCheck.java b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
new file mode 100644
index 0000000..0163e94
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
@@ -0,0 +1,94 @@
+/*
+ * 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.customtabs;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+public abstract class PollingCheck {
+    private static final long TIME_SLICE = 50;
+    private long mTimeout = 3000;
+
+    public interface PollingCheckCondition {
+        boolean canProceed();
+    }
+
+    public PollingCheck() {
+    }
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        while (timeout > 0) {
+            if (condition.call()) {
+                return;
+            }
+
+            Thread.sleep(TIME_SLICE);
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail(message.toString());
+    }
+
+    public static void waitFor(final PollingCheckCondition condition) {
+        new PollingCheck() {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+
+    public static void waitFor(long timeout, final PollingCheckCondition condition) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
new file mode 100644
index 0000000..a2b1b31
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests for {@link PostMessageServiceConnection} with no {@link CustomTabsService} component.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageServiceConnectionTest {
+    @Rule
+    public final ServiceTestRule mServiceRule;
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule;
+    private TestCustomTabsCallback mCallback;
+    private Context mContext;
+    private PostMessageServiceConnection mConnection;
+    private boolean mServiceConnected;
+
+
+    public PostMessageServiceConnectionTest() {
+        mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+        mServiceRule = new ServiceTestRule();
+    }
+
+    @Before
+    public void setup() {
+        mCallback = new TestCustomTabsCallback();
+        mContext = mActivityTestRule.getActivity();
+        mConnection = new PostMessageServiceConnection(
+                new CustomTabsSessionToken(mCallback.getStub())) {
+            public void onPostMessageServiceConnected() {
+                mServiceConnected = true;
+            }
+
+            @Override
+            public void onPostMessageServiceDisconnected() {
+                mServiceConnected = false;
+            }
+        };
+        Intent intent = new Intent();
+        intent.setClassName(mContext.getPackageName(), PostMessageService.class.getName());
+        try {
+            mServiceRule.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        } catch (TimeoutException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void testNotifyChannelCreationAndSendMessages() {
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mServiceConnected;
+            }
+        });
+        assertTrue(mServiceConnected);
+        mConnection.notifyMessageChannelReady(null);
+        assertTrue(mCallback.isMessageChannelReady());
+        mConnection.postMessage("message1", null);
+        assertEquals(mCallback.getMessages().size(), 1);
+        mConnection.postMessage("message2", null);
+        assertEquals(mCallback.getMessages().size(), 2);
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
new file mode 100644
index 0000000..b212693
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * Tests for a complete loop between a browser side {@link CustomTabsService}
+ * and a client side {@link PostMessageService}. Both services are bound to through
+ * {@link ServiceTestRule}, but {@link CustomTabsCallback#extraCallback} is used to link browser
+ * side actions.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageTest {
+    @Rule
+    public final ServiceTestRule mServiceRule;
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule;
+    private TestCustomTabsCallback mCallback;
+    private Context mContext;
+    private CustomTabsServiceConnection mCustomTabsServiceConnection;
+    private PostMessageServiceConnection mPostMessageServiceConnection;
+    private boolean mCustomTabsServiceConnected;
+    private boolean mPostMessageServiceConnected;
+    private CustomTabsSession mSession;
+
+    public PostMessageTest() {
+        mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+        mServiceRule = new ServiceTestRule();
+    }
+
+
+    @Before
+    public void setup() {
+        // Bind to PostMessageService only after CustomTabsService sends the callback to do so. This
+        // callback is sent after requestPostMessageChannel is called.
+        mCallback = new TestCustomTabsCallback() {
+            @Override
+            public void extraCallback(String callbackName, Bundle args) {
+                if (TestCustomTabsService.CALLBACK_BIND_TO_POST_MESSAGE.equals(callbackName)) {
+                    Intent postMessageServiceIntent = new Intent();
+                    postMessageServiceIntent.setClassName(
+                            mContext.getPackageName(), PostMessageService.class.getName());
+                    try {
+                        mServiceRule.bindService(postMessageServiceIntent,
+                                mPostMessageServiceConnection, Context.BIND_AUTO_CREATE);
+                    } catch (TimeoutException e) {
+                        fail();
+                    }
+                }
+            }
+        };
+        mContext = mActivityTestRule.getActivity();
+        mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
+            @Override
+            public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
+                mCustomTabsServiceConnected = true;
+                mSession = client.newSession(mCallback);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName componentName) {
+                mCustomTabsServiceConnected = false;
+            }
+        };
+        mPostMessageServiceConnection = new PostMessageServiceConnection(
+                new CustomTabsSessionToken(mCallback.getStub())) {
+            @Override
+            public void onPostMessageServiceConnected() {
+                mPostMessageServiceConnected = true;
+            }
+
+            @Override
+            public void onPostMessageServiceDisconnected() {
+                mPostMessageServiceConnected = false;
+            }
+        };
+        Intent customTabsServiceIntent = new Intent();
+        customTabsServiceIntent.setClassName(
+                mContext.getPackageName(), TestCustomTabsService.class.getName());
+        try {
+            mServiceRule.bindService(customTabsServiceIntent,
+                    mCustomTabsServiceConnection, Context.BIND_AUTO_CREATE);
+        } catch (TimeoutException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void testCustomTabsConnection() {
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mCustomTabsServiceConnected;
+            }
+        });
+        assertTrue(mCustomTabsServiceConnected);
+        assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
+        assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mPostMessageServiceConnected;
+            }
+        });
+        assertTrue(mPostMessageServiceConnected);
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestActivity.java b/customtabs/tests/src/android/support/customtabs/TestActivity.java
new file mode 100644
index 0000000..1d7941a
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.customtabs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+/**
+ * Simple test activity for custom tabs testing.
+ */
+
+public class TestActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        setContentView(new FrameLayout(this));
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
new file mode 100644
index 0000000..56b1817
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
@@ -0,0 +1,82 @@
+/*
+ * 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.customtabs;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * A test class to check the incoming messages through {@link CustomTabsCallback}.
+ */
+public class TestCustomTabsCallback extends CustomTabsCallback {
+    private boolean mOnMessageChannelReady;
+    private ArrayList<String> mMessageList = new ArrayList<>();
+    private ICustomTabsCallback.Stub mWrapper = new ICustomTabsCallback.Stub() {
+        @Override
+        public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+            TestCustomTabsCallback.this.onNavigationEvent(navigationEvent, extras);
+        }
+
+        @Override
+        public void extraCallback(final String callbackName, final Bundle args)
+                throws RemoteException {
+            TestCustomTabsCallback.this.extraCallback(callbackName, args);
+        }
+
+        @Override
+        public void onMessageChannelReady(final Bundle extras)
+                throws RemoteException {
+            TestCustomTabsCallback.this.onMessageChannelReady(extras);
+        }
+
+        @Override
+        public void onPostMessage(final String message, final Bundle extras)
+                throws RemoteException {
+            TestCustomTabsCallback.this.onPostMessage(message, extras);
+        }
+    };
+
+    /* package */ ICustomTabsCallback getStub() {
+        return mWrapper;
+    }
+
+    @Override
+    public void onMessageChannelReady(Bundle extras) {
+        mOnMessageChannelReady = true;
+    }
+
+    /**
+     * @return Whether the message channel is ready.
+     */
+    public boolean isMessageChannelReady() {
+        return mOnMessageChannelReady;
+    }
+
+    @Override
+    public void onPostMessage(String message, Bundle extras) {
+        mMessageList.add(message);
+    }
+
+    /**
+     * @return A list of messages that have been sent so far.
+     */
+    public ArrayList<String> getMessages() {
+        return mMessageList;
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
new file mode 100644
index 0000000..b5c5e86
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
@@ -0,0 +1,74 @@
+/*
+ * 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.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * A test class that simulates how a {@link CustomTabsService} would behave.
+ */
+
+public class TestCustomTabsService extends CustomTabsService {
+    public static final String CALLBACK_BIND_TO_POST_MESSAGE = "BindToPostMessageService";
+    private boolean mPostMessageRequested;
+    private CustomTabsSessionToken mSession;
+
+    @Override
+    protected boolean warmup(long flags) {
+        return false;
+    }
+
+    @Override
+    protected boolean newSession(CustomTabsSessionToken sessionToken) {
+        mSession = sessionToken;
+        return true;
+    }
+
+    @Override
+    protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken,
+                                   Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
+        return false;
+    }
+
+    @Override
+    protected Bundle extraCommand(String commandName, Bundle args) {
+        return null;
+    }
+
+    @Override
+    protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) {
+        return false;
+    }
+
+    @Override
+    protected boolean requestPostMessageChannel(
+            CustomTabsSessionToken sessionToken, Uri postMessageOrigin) {
+        if (mSession == null) return false;
+        mPostMessageRequested = true;
+        mSession.getCallback().extraCallback(CALLBACK_BIND_TO_POST_MESSAGE, null);
+        return true;
+    }
+
+    @Override
+    protected int postMessage(CustomTabsSessionToken sessionToken, String message, Bundle extras) {
+        if (!mPostMessageRequested) return CustomTabsService.RESULT_FAILURE_DISALLOWED;
+        return CustomTabsService.RESULT_SUCCESS;
+    }
+}
diff --git a/design/src/android/support/design/widget/TabLayout.java b/design/src/android/support/design/widget/TabLayout.java
index b6f94a4..d3a8dd1 100755
--- a/design/src/android/support/design/widget/TabLayout.java
+++ b/design/src/android/support/design/widget/TabLayout.java
@@ -1084,17 +1084,7 @@
         final int targetScrollX = calculateScrollXForTab(newPosition, 0);
 
         if (startScrollX != targetScrollX) {
-            if (mScrollAnimator == null) {
-                mScrollAnimator = ViewUtils.createAnimator();
-                mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
-                mScrollAnimator.setDuration(ANIMATION_DURATION);
-                mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimatorCompat animator) {
-                        scrollTo(animator.getAnimatedIntValue(), 0);
-                    }
-                });
-            }
+            ensureScrollAnimator();
 
             mScrollAnimator.setIntValues(startScrollX, targetScrollX);
             mScrollAnimator.start();
@@ -1104,6 +1094,25 @@
         mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
     }
 
+    private void ensureScrollAnimator() {
+        if (mScrollAnimator == null) {
+            mScrollAnimator = ViewUtils.createAnimator();
+            mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+            mScrollAnimator.setDuration(ANIMATION_DURATION);
+            mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimatorCompat animator) {
+                    scrollTo(animator.getAnimatedIntValue(), 0);
+                }
+            });
+        }
+    }
+
+    void setScrollAnimatorListener(ValueAnimatorCompat.AnimatorListener listener) {
+        ensureScrollAnimator();
+        mScrollAnimator.addListener(listener);
+    }
+
     private void setSelectedTabView(int position) {
         final int tabCount = mTabStrip.getChildCount();
         if (position < tabCount) {
@@ -1177,10 +1186,14 @@
             final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
             final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
 
-            return selectedChild.getLeft()
-                    + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
-                    + (selectedChild.getWidth() / 2)
-                    - (getWidth() / 2);
+            // base scroll amount: places center of tab in center of parent
+            int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
+            // offset amount: fraction of the distance between centers of tabs
+            int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
+
+            return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
+                    ? scrollBase + scrollOffset
+                    : scrollBase - scrollOffset;
         }
         return 0;
     }
diff --git a/design/tests/res/layout/design_tabs_fixed_width.xml b/design/tests/res/layout/design_tabs_fixed_width.xml
new file mode 100644
index 0000000..752034f
--- /dev/null
+++ b/design/tests/res/layout/design_tabs_fixed_width.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<!-- Width is fixed to test scrolling -->
+<android.support.design.widget.TabLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/tabs"
+    android:layout_width="160dp"
+    android:layout_height="wrap_content"
+    app:tabMode="scrollable">
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 0" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 1" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 2" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 3" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 4" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 5" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 6" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 7" />
+
+</android.support.design.widget.TabLayout>
diff --git a/design/tests/src/android/support/design/testutils/TabLayoutActions.java b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
index 149b14f..7c17850 100644
--- a/design/tests/src/android/support/design/testutils/TabLayoutActions.java
+++ b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
@@ -16,23 +16,17 @@
 
 package android.support.design.testutils;
 
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+
 import android.support.annotation.Nullable;
 import android.support.design.widget.TabLayout;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
-import android.support.test.espresso.action.CoordinatesProvider;
-import android.support.test.espresso.action.GeneralClickAction;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Tap;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.PagerTitleStrip;
+import android.support.test.espresso.matcher.ViewMatchers;
 import android.support.v4.view.ViewPager;
 import android.view.View;
-import android.widget.TextView;
-import org.hamcrest.Matcher;
 
-import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import org.hamcrest.Matcher;
 
 public class TabLayoutActions {
     /**
@@ -143,4 +137,30 @@
             }
         };
     }
+
+    /**
+     * Calls <code>setScrollPosition(position, positionOffset, true)</code> on the
+     * <code>TabLayout</code>
+     */
+    public static ViewAction setScrollPosition(final int position, final float positionOffset) {
+        return new ViewAction() {
+
+            @Override
+            public Matcher<View> getConstraints() {
+                return ViewMatchers.isAssignableFrom(TabLayout.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "setScrollPosition(" + position + ", " + positionOffset + ", true)";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                TabLayout tabs = (TabLayout) view;
+                tabs.setScrollPosition(position, positionOffset, true);
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
 }
diff --git a/design/tests/src/android/support/design/widget/TabLayoutTest.java b/design/tests/src/android/support/design/widget/TabLayoutTest.java
index 539ab86..e9255fa 100755
--- a/design/tests/src/android/support/design/widget/TabLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutTest.java
@@ -16,6 +16,12 @@
 
 package android.support.design.widget;
 
+import static android.support.design.testutils.TabLayoutActions.selectTab;
+import static android.support.design.testutils.TabLayoutActions.setScrollPosition;
+import static android.support.design.testutils.TestUtilsActions.setLayoutDirection;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -29,7 +35,12 @@
 
 import android.support.design.test.R;
 import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.Espresso;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.NoMatchingViewException;
+import android.support.test.espresso.ViewAssertion;
 import android.support.test.filters.SmallTest;
+import android.support.v4.view.ViewCompat;
 import android.support.v7.app.AppCompatActivity;
 import android.view.InflateException;
 import android.view.LayoutInflater;
@@ -37,6 +48,8 @@
 
 import org.junit.Test;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 @SmallTest
 public class TabLayoutTest extends BaseInstrumentationTestCase<AppCompatActivity> {
     public TabLayoutTest() {
@@ -188,4 +201,105 @@
             }
         }
     }
+
+    @Test
+    public void setScrollPositionLtr() throws Throwable {
+        testSetScrollPosition(true);
+    }
+
+    @Test
+    public void setScrollPositionRtl() throws Throwable {
+        testSetScrollPosition(false);
+    }
+
+    private void testSetScrollPosition(final boolean isLtr) throws Throwable {
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width);
+            }
+        });
+        final TabLayout tabs = (TabLayout) mActivityTestRule.getActivity().findViewById(R.id.tabs);
+        assertEquals(TabLayout.MODE_SCROLLABLE, tabs.getTabMode());
+
+        final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs);
+        Espresso.registerIdlingResources(idler);
+
+        // We're going to call setScrollPosition() incrementally, as if scrolling between one tab
+        // and the next. Use the middle tab for best results. The positionOffsets should be in the
+        // range [0, 1), so the final call will wrap to 0 but use the next tab's position.
+        final int middleTab = tabs.getTabCount() / 2;
+        final int[] positions = {middleTab, middleTab, middleTab, middleTab, middleTab + 1};
+        final float[] positionOffsets = {0f, .25f, .5f, .75f, 0f};
+
+        // Set layout direction
+        onView(withId(R.id.tabs)).perform(setLayoutDirection(
+                isLtr ? ViewCompat.LAYOUT_DIRECTION_LTR : ViewCompat.LAYOUT_DIRECTION_RTL));
+        // Make sure it's scrolled all the way to the start
+        onView(withId(R.id.tabs)).perform(selectTab(0));
+
+        // Perform a series of setScrollPosition() calls
+        final AtomicInteger lastScrollX = new AtomicInteger(tabs.getScrollX());
+        for (int i = 0; i < positions.length; i++) {
+            onView(withId(R.id.tabs))
+                    .perform(setScrollPosition(positions[i], positionOffsets[i]))
+                    .check(new ViewAssertion() {
+                        @Override
+                        public void check(View view, NoMatchingViewException notFoundException) {
+                            if (view == null) {
+                                throw notFoundException;
+                            }
+                            // Verify increasing or decreasing scroll X values
+                            int sx = view.getScrollX();
+                            assertTrue(isLtr ? sx > lastScrollX.get() : sx < lastScrollX.get());
+                            lastScrollX.set(sx);
+                        }
+                    });
+        }
+
+        Espresso.unregisterIdlingResources(idler);
+    }
+
+    static class TabLayoutScrollIdlingResource implements IdlingResource {
+
+        private boolean mIsIdle = true;
+        private ResourceCallback mCallback;
+
+        TabLayoutScrollIdlingResource(final TabLayout tabLayout) {
+            tabLayout.setScrollAnimatorListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(ValueAnimatorCompat animator) {
+                    setIdle(false);
+                }
+
+                @Override
+                public void onAnimationEnd(ValueAnimatorCompat animator) {
+                    setIdle(true);
+                }
+            });
+        }
+
+        @Override
+        public String getName() {
+            return "TabLayoutScrollIdlingResource";
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            return mIsIdle;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback callback) {
+            mCallback = callback;
+        }
+
+        private void setIdle(boolean idle) {
+            boolean wasIdle = mIsIdle;
+            mIsIdle = idle;
+            if (mIsIdle && !wasIdle && mCallback != null) {
+                mCallback.onTransitionToIdle();
+            }
+        }
+    }
 }
diff --git a/exifinterface/build.gradle b/exifinterface/build.gradle
index c761832..c65cf71 100644
--- a/exifinterface/build.gradle
+++ b/exifinterface/build.gradle
@@ -19,7 +19,6 @@
     sourceSets {
         main.manifest.srcFile 'AndroidManifest.xml'
         main.java.srcDirs = ['src']
-        main.res.srcDirs = ['res']
 
         androidTest.setRoot('tests')
         androidTest.java.srcDir 'tests/src'
diff --git a/exifinterface/src/android/support/media/ExifInterface.java b/exifinterface/src/android/support/media/ExifInterface.java
index fccab43..4830ecc 100644
--- a/exifinterface/src/android/support/media/ExifInterface.java
+++ b/exifinterface/src/android/support/media/ExifInterface.java
@@ -1850,11 +1850,31 @@
     }
 
     /**
-     * Stores the latitude and longitude value in a float array. The first element is
-     * the latitude, and the second element is the longitude. Returns false if the
-     * Exif tags are not available.
+     * Stores the latitude and longitude value in a float array. The first element is the latitude,
+     * and the second element is the longitude. Returns false if the Exif tags are not available.
+     *
+     * @deprecated Use {@link #getLatLong()} instead.
      */
+    @Deprecated
     public boolean getLatLong(float output[]) {
+        double[] latLong = getLatLong();
+        if (latLong == null) {
+            return false;
+        }
+
+        output[0] = (float) latLong[0];
+        output[1] = (float) latLong[1];
+        return true;
+    }
+
+    /**
+     * Gets the latitude and longitude values.
+     * <p>
+     * If there are valid latitude and longitude values in the image, this method returns a double
+     * array where the first element is the latitude and the second element is the longitude.
+     * Otherwise, it returns null.
+     */
+    public double[] getLatLong() {
         String latValue = getAttribute(TAG_GPS_LATITUDE);
         String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
         String lngValue = getAttribute(TAG_GPS_LONGITUDE);
@@ -1862,16 +1882,39 @@
 
         if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
             try {
-                output[0] = convertRationalLatLonToFloat(latValue, latRef);
-                output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
-                return true;
+                double latitude = convertRationalLatLonToDouble(latValue, latRef);
+                double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
+                return new double[] {latitude, longitude};
             } catch (IllegalArgumentException e) {
                 Log.w(TAG, "Latitude/longitude values are not parseable. " +
                         String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
                                 latValue, latRef, lngValue, lngRef));
             }
         }
-        return false;
+        return null;
+    }
+
+    /**
+     * Sets the latitude and longitude values.
+     *
+     * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
+     *                 90.0.
+     * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
+     *                  and 180.0.
+     * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
+     *                                  specified range.
+     */
+    public void setLatLong(double latitude, double longitude) {
+        if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
+            throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
+        }
+        if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
+            throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
+        }
+        setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
+        setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
+        setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
+        setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
     }
 
     /**
@@ -1953,7 +1996,7 @@
         }
     }
 
-    private static float convertRationalLatLonToFloat(String rationalString, String ref) {
+    private static double convertRationalLatLonToDouble(String rationalString, String ref) {
         try {
             String [] parts = rationalString.split(",");
 
@@ -1972,15 +2015,26 @@
 
             double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
             if ((ref.equals("S") || ref.equals("W"))) {
-                return (float) -result;
+                return -result;
+            } else if (ref.equals("N") || ref.equals("E")) {
+                return result;
+            } else {
+                // Not valid
+                throw new IllegalArgumentException();
             }
-            return (float) result;
         } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
             // Not valid
             throw new IllegalArgumentException();
         }
     }
 
+    private String convertDecimalDegree(double decimalDegree) {
+        long degrees = (long) decimalDegree;
+        long minutes = (long) ((decimalDegree - degrees) * 60.0);
+        long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
+        return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
+    }
+
     // Checks the type of image file
     private int getMimeType(BufferedInputStream in) throws IOException {
         in.mark(SIGNATURE_CHECK_SIZE);
diff --git a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
index 87fb950..ee9e38e 100644
--- a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
+++ b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
@@ -16,6 +16,13 @@
 
 package android.support.media;
 
+import static android.support.test.InstrumentationRegistry.getContext;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.os.Environment;
@@ -40,12 +47,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 
-import static android.support.test.InstrumentationRegistry.getContext;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.fail;
-
 /**
  * Test {@link ExifInterface}.
  */
@@ -53,7 +54,6 @@
 public class ExifInterfaceTest {
     private static final String TAG = ExifInterface.class.getSimpleName();
     private static final boolean VERBOSE = false;  // lots of logging
-
     private static final double DIFFERENCE_TOLERANCE = .001;
 
     private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
@@ -64,6 +64,20 @@
     private static final String[] IMAGE_FILENAMES = new String[] {
             EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG};
 
+    private static final String TEST_TEMP_FILE_NAME = "testImage";
+    private static final double DELTA = 1e-8;
+    private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8;
+    private static final double[] TEST_LATITUDE_VALID_VALUES = new double[]
+            {0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737};
+    private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[]
+            {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352};
+    private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[]
+            {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001,
+                    263.34763236326, -1e5, 347.32525, -176.346347754};
+    private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[]
+            {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001,
+                    263.34763236326, -1e10, 347.325252623, -4000.346323236};
+
     private static final String[] EXIF_TAGS = {
             ExifInterface.TAG_MAKE,
             ExifInterface.TAG_MODEL,
@@ -235,6 +249,52 @@
         }
     }
 
+    @Test
+    @SmallTest
+    public void testSetLatLong_withValidValues() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+
+            double[] latLong = exif.getLatLong();
+            assertNotNull(latLong);
+            assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA);
+            assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSetLatLong_withInvalidLatitude() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            try {
+                exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            assertNull(exif.getLatLong());
+            assertLatLongValuesAreNotSet(exif);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSetLatLong_withInvalidLongitude() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            try {
+                exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            assertNull(exif.getLatLong());
+            assertLatLongValuesAreNotSet(exif);
+        }
+    }
+
     private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
         // Prints thumbnail information.
         if (exifInterface.hasThumbnail()) {
@@ -264,8 +324,8 @@
         // Prints GPS information.
         Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
 
-        float[] latLong = new float[2];
-        if (exifInterface.getLatLong(latLong)) {
+        double[] latLong = exifInterface.getLatLong();
+        if (latLong != null) {
             Log.v(TAG, fileName + " Latitude = " + latLong[0]);
             Log.v(TAG, fileName + " Longitude = " + latLong[1]);
         } else {
@@ -318,8 +378,8 @@
         }
 
         // Checks GPS information.
-        float[] latLong = new float[2];
-        assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+        double[] latLong = exifInterface.getLatLong();
+        assertEquals(expectedValue.hasLatLong, latLong != null);
         if (expectedValue.hasLatLong) {
             assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
             assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
@@ -443,4 +503,17 @@
         }
         return total;
     }
+
+    private void assertLatLongValuesAreNotSet(ExifInterface exif) {
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF));
+    }
+
+    private ExifInterface createTestExifInterface() throws IOException {
+        File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg");
+        image.deleteOnExit();
+        return new ExifInterface(image.getAbsolutePath());
+    }
 }
diff --git a/fragment/java/android/support/v4/app/BackStackRecord.java b/fragment/java/android/support/v4/app/BackStackRecord.java
index 326521a..923c366 100644
--- a/fragment/java/android/support/v4/app/BackStackRecord.java
+++ b/fragment/java/android/support/v4/app/BackStackRecord.java
@@ -762,8 +762,11 @@
     /**
      * Reverses the execution of the operations within this transaction. The Fragment states will
      * only be modified if optimizations are not allowed.
+     *
+     * @param moveToState {@code true} if added fragments should be moved to their final state
+     *                    in unoptimized transactions
      */
-    void executePopOps() {
+    void executePopOps(boolean moveToState) {
         for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
             final Op op = mOps.get(opNum);
             Fragment f = op.fragment;
@@ -800,7 +803,7 @@
                 mManager.moveFragmentToExpectedState(f);
             }
         }
-        if (!mAllowOptimization) {
+        if (!mAllowOptimization && moveToState) {
             mManager.moveToState(mManager.mCurState, true);
         }
     }
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index 91d1d91..d0eef59 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -2230,7 +2230,7 @@
                 if (isPop) {
                     record.executeOps();
                 } else {
-                    record.executePopOps();
+                    record.executePopOps(false);
                 }
 
                 // move to the end
@@ -2349,7 +2349,10 @@
             final boolean isPop = isRecordPop.get(i);
             if (isPop) {
                 record.bumpBackStackNesting(-1);
-                record.executePopOps();
+                // Only execute the add operations at the end of
+                // all transactions.
+                boolean moveToState = i == (endIndex - 1);
+                record.executePopOps(moveToState);
             } else {
                 record.bumpBackStackNesting(1);
                 record.executeOps();
diff --git a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
index 211ec35..ace24e9 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
@@ -1036,6 +1036,10 @@
 
         FragmentTestUtil.executePendingTransactions(mActivityRule);
 
+        assertEquals(1, fragment1.onCreateViewCount);
+        assertEquals(1, fragment2.onCreateViewCount);
+        assertEquals(1, fragment3.onCreateViewCount);
+
         FragmentTestUtil.popBackStackImmediate(mActivityRule, "two",
                 FragmentManager.POP_BACK_STACK_INCLUSIVE);
 
@@ -1043,6 +1047,10 @@
                 mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
 
         FragmentTestUtil.assertChildren(container, fragment1);
+
+        assertEquals(2, fragment1.onCreateViewCount);
+        assertEquals(1, fragment2.onCreateViewCount);
+        assertEquals(1, fragment3.onCreateViewCount);
     }
 
     private View findViewById(int viewId) {
@@ -1091,10 +1099,13 @@
     }
 
     public static class SimpleViewFragment extends Fragment {
+        public int onCreateViewCount;
+
         @Nullable
         @Override
         public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                 @Nullable Bundle savedInstanceState) {
+            onCreateViewCount++;
             return inflater.inflate(R.layout.fragment_a, container, false);
         }
     }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index c2fa6ab..243f016 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -121,7 +121,9 @@
      */
     public MediaBrowserCompat(Context context, ComponentName serviceComponent,
             ConnectionCallback callback, Bundle rootHints) {
-        if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+        // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
+        // and 25 devices, use the support library version of implementation on those devices.
+        if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
             mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints);
         } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
@@ -573,7 +575,7 @@
         WeakReference<Subscription> mSubscriptionRef;
 
         public SubscriptionCallback() {
-            if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+            if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
                 mSubscriptionCallbackObj =
                         MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24());
                 mToken = null;
@@ -1346,9 +1348,9 @@
 
         public MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
-            // Do not send the client version for API 25 and higher, since we don't need to use
-            // EXTRA_MESSENGER_BINDER for API 24 and higher.
-            if (Build.VERSION.SDK_INT < 25) {
+            // Do not send the client version for API 26 and higher, since we don't need to use
+            // EXTRA_MESSENGER_BINDER for API 26 and higher.
+            if (Build.VERSION.SDK_INT <= 25) {
                 if (rootHints == null) {
                     rootHints = new Bundle();
                 }
@@ -1422,6 +1424,8 @@
             sub.putCallback(copiedOptions, callback);
 
             if (mServiceBinderWrapper == null) {
+                // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
+                // subscribe with options won't work properly.
                 MediaBrowserCompatApi21.subscribe(
                         mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
             } else {
@@ -1625,6 +1629,7 @@
         }
     }
 
+    // TODO: Rename to MediaBrowserImplApi26 once O is released
     static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 {
         public MediaBrowserImplApi24(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 77a6e19..22c3d55 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -355,6 +355,7 @@
         }
     }
 
+    // TODO: Rename to MediaBrowserServiceImplApi26 once O is released
     class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
             MediaBrowserServiceCompatApi24.ServiceCompatProxy {
         @Override
@@ -786,7 +787,7 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+        if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
             mImpl = new MediaBrowserServiceImplApi24();
         } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserServiceImplApi23();
diff --git a/media-compat/java/android/support/v4/media/TransportMediator.java b/media-compat/java/android/support/v4/media/TransportMediator.java
index 177f6aa..ec3baec 100644
--- a/media-compat/java/android/support/v4/media/TransportMediator.java
+++ b/media-compat/java/android/support/v4/media/TransportMediator.java
@@ -83,28 +83,28 @@
     public static final int KEYCODE_MEDIA_RECORD = 130;
 
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
-     * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS */
+     * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS} */
     public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
-     * RemoteControlClient.FLAG_KEY_MEDIA_REWIND */
+     * RemoteControlClient.FLAG_KEY_MEDIA_REWIND} */
     public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
-     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY */
+     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY} */
     public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
-     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */
+     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE} */
     public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
-     * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE */
+     * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE} */
     public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
-     * RemoteControlClient.FLAG_KEY_MEDIA_STOP */
+     * RemoteControlClient.FLAG_KEY_MEDIA_STOP} */
     public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
-     * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */
+     * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD} */
     public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
     /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
-     * RemoteControlClient.FLAG_KEY_MEDIA_NEXT */
+     * RemoteControlClient.FLAG_KEY_MEDIA_NEXT} */
     public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
 
     static boolean isMediaKey(int keyCode) {
diff --git a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
index d905350..d1d143d 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
@@ -37,4 +37,6 @@
     void onQueueTitleChanged(CharSequence title);
     void onExtrasChanged(in Bundle extras);
     void onVolumeInfoChanged(in ParcelableVolumeInfo info);
+    void onRepeatModeChanged(int repeatMode);
+    void onShuffleModeChanged(boolean enabled);
 }
diff --git a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
index c7705e8..4f2e38a 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
@@ -52,6 +52,8 @@
     CharSequence getQueueTitle() = 29;
     Bundle getExtras() = 30;
     int getRatingType() = 31;
+    int getRepeatMode() = 36;
+    boolean isShuffleModeEnabled() = 37;
 
     // These commands are for the TransportControls
     void prepare() = 32;
@@ -71,5 +73,7 @@
     void rewind() = 22;
     void seekTo(long pos) = 23;
     void rate(in RatingCompat rating) = 24;
+    void setRepeatMode(int repeatMode) = 38;
+    void setShuffleModeEnabled(boolean shuffleMode) = 39;
     void sendCustomAction(String action, in Bundle args) = 25;
 }
diff --git a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
index 74658a8..3e0bf4d 100644
--- a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -269,6 +269,25 @@
     }
 
     /**
+     * Get the repeat mode for this session.
+     *
+     * @return The latest repeat mode set to the session, or
+     *         {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
+     */
+    public int getRepeatMode() {
+        return mImpl.getRepeatMode();
+    }
+
+    /**
+     * Return whether the shuffle mode is enabled for this session.
+     *
+     * @return {@code true} if the shuffle mode is enabled, {@code false} if disabled or not set.
+     */
+    public boolean isShuffleModeEnabled() {
+        return mImpl.isShuffleModeEnabled();
+    }
+
+    /**
      * Get the flags for this session. Flags are defined in
      * {@link MediaSessionCompat}.
      *
@@ -510,6 +529,25 @@
         public void onAudioInfoChanged(PlaybackInfo info) {
         }
 
+        /**
+         * Override to handle changes to the repeat mode.
+         *
+         * @param repeatMode The repeat mode. It should be one of followings:
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        }
+
+        /**
+         * Override to handle changes to the shuffle mode.
+         *
+         * @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise.
+         */
+        public void onShuffleModeChanged(boolean enabled) {
+        }
+
         @Override
         public void binderDied() {
             onSessionDestroyed();
@@ -614,6 +652,16 @@
             }
 
             @Override
+            public void onRepeatModeChanged(int repeatMode) throws RemoteException {
+                mHandler.post(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
+            }
+
+            @Override
+            public void onShuffleModeChanged(boolean enabled) throws RemoteException {
+                mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, enabled, null);
+            }
+
+            @Override
             public void onExtrasChanged(Bundle extras) throws RemoteException {
                 mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
             }
@@ -638,6 +686,8 @@
             private static final int MSG_UPDATE_QUEUE_TITLE = 6;
             private static final int MSG_UPDATE_EXTRAS = 7;
             private static final int MSG_DESTROYED = 8;
+            private static final int MSG_UPDATE_REPEAT_MODE = 9;
+            private static final int MSG_UPDATE_SHUFFLE_MODE = 10;
 
             public MessageHandler(Looper looper) {
                 super(looper);
@@ -664,6 +714,12 @@
                     case MSG_UPDATE_QUEUE_TITLE:
                         onQueueTitleChanged((CharSequence) msg.obj);
                         break;
+                    case MSG_UPDATE_REPEAT_MODE:
+                        onRepeatModeChanged((int) msg.obj);
+                        break;
+                    case MSG_UPDATE_SHUFFLE_MODE:
+                        onShuffleModeChanged((boolean) msg.obj);
+                        break;
                     case MSG_UPDATE_EXTRAS:
                         onExtrasChanged((Bundle) msg.obj);
                         break;
@@ -839,6 +895,23 @@
         public abstract void setRating(RatingCompat rating);
 
         /**
+         * Set the repeat mode for this session.
+         *
+         * @param repeatMode The repeat mode. Must be one of the followings:
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+
+        /**
+         * Set the shuffle mode for this session.
+         *
+         * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+         */
+        public abstract void setShuffleModeEnabled(boolean enabled);
+
+        /**
          * Send a custom action for the {@link MediaSessionCompat} to perform.
          *
          * @param customAction The action to perform.
@@ -963,6 +1036,8 @@
         CharSequence getQueueTitle();
         Bundle getExtras();
         int getRatingType();
+        int getRepeatMode();
+        boolean isShuffleModeEnabled();
         long getFlags();
         PlaybackInfo getPlaybackInfo();
         PendingIntent getSessionActivity();
@@ -1099,6 +1174,26 @@
         }
 
         @Override
+        public int getRepeatMode() {
+            try {
+                return mBinder.getRepeatMode();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in getRepeatMode. " + e);
+            }
+            return 0;
+        }
+
+        @Override
+        public boolean isShuffleModeEnabled() {
+            try {
+                return mBinder.isShuffleModeEnabled();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in isShuffleModeEnabled. " + e);
+            }
+            return false;
+        }
+
+        @Override
         public long getFlags() {
             try {
                 return mBinder.getFlags();
@@ -1336,6 +1431,24 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            try {
+                mBinder.setRepeatMode(repeatMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in setRepeatMode. " + e);
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            try {
+                mBinder.setShuffleModeEnabled(enabled);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in setShuffleModeEnabled. " + e);
+            }
+        }
+
+        @Override
         public void sendCustomAction(CustomAction customAction, Bundle args) {
             sendCustomAction(customAction.getAction(), args);
         }
@@ -1476,6 +1589,30 @@
         }
 
         @Override
+        public int getRepeatMode() {
+            if (mExtraBinder != null) {
+                try {
+                    return mExtraBinder.getRepeatMode();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Dead object in getRepeatMode. " + e);
+                }
+            }
+            return PlaybackStateCompat.REPEAT_MODE_NONE;
+        }
+
+        @Override
+        public boolean isShuffleModeEnabled() {
+            if (mExtraBinder != null) {
+                try {
+                    return mExtraBinder.isShuffleModeEnabled();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Dead object in isShuffleModeEnabled. " + e);
+                }
+            }
+            return false;
+        }
+
+        @Override
         public long getFlags() {
             return MediaControllerCompatApi21.getFlags(mControllerObj);
         }
@@ -1619,6 +1756,26 @@
             }
 
             @Override
+            public void onRepeatModeChanged(final int repeatMode) throws RemoteException {
+                mCallback.mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onRepeatModeChanged(repeatMode);
+                    }
+                });
+            }
+
+            @Override
+            public void onShuffleModeChanged(final boolean enabled) throws RemoteException {
+                mCallback.mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onShuffleModeChanged(enabled);
+                    }
+                });
+            }
+
+            @Override
             public void onExtrasChanged(Bundle extras) throws RemoteException {
                 // Will not be called.
                 throw new AssertionError();
@@ -1715,6 +1872,20 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            Bundle bundle = new Bundle();
+            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
+            sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED, enabled);
+            sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE_ENABLED, bundle);
+        }
+
+        @Override
         public void playFromMediaId(String mediaId, Bundle extras) {
             MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
                     extras);
@@ -1833,5 +2004,4 @@
             MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
         }
     }
-
 }
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index 53e6c70..c560c1b 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -140,6 +140,18 @@
             "android.support.v4.media.session.action.PREPARE_FROM_URI";
 
     /**
+     * Custom action to invoke setRepeatMode() for the forward compatibility.
+     */
+    static final String ACTION_SET_REPEAT_MODE =
+            "android.support.v4.media.session.action.SET_REPEAT_MODE";
+
+    /**
+     * Custom action to invoke setShuffleModeEnabled() for the forward compatibility.
+     */
+    static final String ACTION_SET_SHUFFLE_MODE_ENABLED =
+            "android.support.v4.media.session.action.SET_SHUFFLE_MODE_ENABLED";
+
+    /**
      * Argument for use with {@link #ACTION_PREPARE_FROM_MEDIA_ID} indicating media id to play.
      */
     static final String ACTION_ARGUMENT_MEDIA_ID =
@@ -164,6 +176,19 @@
     static final String ACTION_ARGUMENT_EXTRAS =
             "android.support.v4.media.session.action.ARGUMENT_EXTRAS";
 
+    /**
+     * Argument for use with {@link #ACTION_SET_REPEAT_MODE} indicating repeat mode.
+     */
+    static final String ACTION_ARGUMENT_REPEAT_MODE =
+            "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE";
+
+    /**
+     * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE_ENABLED} indicating that shuffle mode
+     * is enabled.
+     */
+    static final String ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED =
+            "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE_ENABLED";
+
     static final String EXTRA_BINDER = "android.support.v4.media.session.EXTRA_BINDER";
 
     // Maximum size of the bitmap in dp.
@@ -238,6 +263,8 @@
         if (android.os.Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaSessionImplApi21(context, tag);
             mImpl.setMediaButtonReceiver(mbrIntent);
+            // Set default callback to respond to controllers' extra binder requests.
+            setCallback(new Callback() {});
         } else {
             mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent);
         }
@@ -501,6 +528,33 @@
     }
 
     /**
+     * Set the repeat mode for this session.
+     * <p>
+     * Note that if this method is not called before, {@link MediaControllerCompat#getRepeatMode}
+     * will return {@link PlaybackStateCompat#REPEAT_MODE_NONE}.
+     *
+     * @param repeatMode The repeat mode. Must be one of the followings:
+     *            {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+     *            {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+     *            {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+     */
+    public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        mImpl.setRepeatMode(repeatMode);
+    }
+
+    /**
+     * Set the shuffle mode for this session.
+     * <p>
+     * Note that if this method is not called before,
+     * {@link MediaControllerCompat#isShuffleModeEnabled} will return {@code false}.
+     *
+     * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+     */
+    public void setShuffleModeEnabled(boolean enabled) {
+        mImpl.setShuffleModeEnabled(enabled);
+    }
+
+    /**
      * Set some extras that can be associated with the
      * {@link MediaSessionCompat}. No assumptions should be made as to how a
      * {@link MediaControllerCompat} will handle these extras. Keys should be
@@ -787,6 +841,33 @@
         }
 
         /**
+         * Override to handle the setting of the repeat mode.
+         * <p>
+         * You should call {@link #setRepeatMode} before end of this method in order to notify
+         * the change to the {@link MediaControllerCompat}, or
+         * {@link MediaControllerCompat#getRepeatMode} could return an invalid value.
+         *
+         * @param repeatMode The repeat mode which is one of followings:
+         *            {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *            {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *            {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        }
+
+        /**
+         * Override to handle the setting of the shuffle mode.
+         * <p>
+         * You should call {@link #setShuffleModeEnabled} before the end of this method in order to
+         * notify the change to the {@link MediaControllerCompat}, or
+         * {@link MediaControllerCompat#isShuffleModeEnabled} could return an invalid value.
+         *
+         * @param enabled true when the shuffle mode is enabled, false otherwise.
+         */
+        public void onSetShuffleModeEnabled(boolean enabled) {
+        }
+
+        /**
          * Called when a {@link MediaControllerCompat} wants a
          * {@link PlaybackStateCompat.CustomAction} to be performed.
          *
@@ -902,6 +983,12 @@
                     Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI);
                     Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS);
                     Callback.this.onPrepareFromUri(uri, bundle);
+                } else if (action.equals(ACTION_SET_REPEAT_MODE)) {
+                    int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE);
+                    Callback.this.onSetRepeatMode(repeatMode);
+                } else if (action.equals(ACTION_SET_SHUFFLE_MODE_ENABLED)) {
+                    boolean enabled = extras.getBoolean(ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED);
+                    Callback.this.onSetShuffleModeEnabled(enabled);
                 } else {
                     Callback.this.onCustomAction(action, extras);
                 }
@@ -1281,6 +1368,8 @@
         void setQueueTitle(CharSequence title);
 
         void setRatingType(@RatingCompat.Style int type);
+        void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+        void setShuffleModeEnabled(boolean enabled);
         void setExtras(Bundle extras);
 
         Object getMediaSession();
@@ -1320,6 +1409,8 @@
         List<QueueItem> mQueue;
         CharSequence mQueueTitle;
         @RatingCompat.Style int mRatingType;
+        @PlaybackStateCompat.RepeatMode int mRepeatMode;
+        boolean mShuffleModeEnabled;
         Bundle mExtras;
 
         int mVolumeType;
@@ -1607,6 +1698,22 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            if (mRepeatMode != repeatMode) {
+                mRepeatMode = repeatMode;
+                sendRepeatMode(repeatMode);
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            if (mShuffleModeEnabled != enabled) {
+                mShuffleModeEnabled = enabled;
+                sendShuffleModeEnabled(enabled);
+            }
+        }
+
+        @Override
         public void setExtras(Bundle extras) {
             mExtras = extras;
             sendExtras(extras);
@@ -1825,6 +1932,30 @@
             mControllerCallbacks.finishBroadcast();
         }
 
+        private void sendRepeatMode(int repeatMode) {
+            int size = mControllerCallbacks.beginBroadcast();
+            for (int i = size - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+                try {
+                    cb.onRepeatModeChanged(repeatMode);
+                } catch (RemoteException e) {
+                }
+            }
+            mControllerCallbacks.finishBroadcast();
+        }
+
+        private void sendShuffleModeEnabled(boolean enabled) {
+            int size = mControllerCallbacks.beginBroadcast();
+            for (int i = size - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+                try {
+                    cb.onShuffleModeChanged(enabled);
+                } catch (RemoteException e) {
+                }
+            }
+            mControllerCallbacks.finishBroadcast();
+        }
+
         private void sendExtras(Bundle extras) {
             int size = mControllerCallbacks.beginBroadcast();
             for (int i = size - 1; i >= 0; i--) {
@@ -2021,6 +2152,16 @@
             }
 
             @Override
+            public void setRepeatMode(int repeatMode) throws RemoteException {
+                postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode);
+            }
+
+            @Override
+            public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+                postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE_ENABLED, enabled);
+            }
+
+            @Override
             public void sendCustomAction(String action, Bundle args)
                     throws RemoteException {
                 postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args);
@@ -2062,6 +2203,17 @@
             }
 
             @Override
+            @PlaybackStateCompat.RepeatMode
+            public int getRepeatMode() {
+                return mRepeatMode;
+            }
+
+            @Override
+            public boolean isShuffleModeEnabled() {
+                return mShuffleModeEnabled;
+            }
+
+            @Override
             public boolean isTransportControlEnabled() {
                 return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0;
             }
@@ -2103,6 +2255,8 @@
             private static final int MSG_CUSTOM_ACTION = 20;
             private static final int MSG_MEDIA_BUTTON = 21;
             private static final int MSG_SET_VOLUME = 22;
+            private static final int MSG_SET_REPEAT_MODE = 23;
+            private static final int MSG_SET_SHUFFLE_MODE_ENABLED = 24;
 
             // KeyEvent constants only available on API 11+
             private static final int KEYCODE_MEDIA_PAUSE = 127;
@@ -2210,6 +2364,12 @@
                     case MSG_SET_VOLUME:
                         setVolumeTo((int) msg.obj, 0);
                         break;
+                    case MSG_SET_REPEAT_MODE:
+                        cb.onSetRepeatMode((int) msg.obj);
+                        break;
+                    case MSG_SET_SHUFFLE_MODE_ENABLED:
+                        cb.onSetShuffleModeEnabled((boolean) msg.obj);
+                        break;
                 }
             }
 
@@ -2286,6 +2446,8 @@
 
         private PlaybackStateCompat mPlaybackState;
         @RatingCompat.Style int mRatingType;
+        @PlaybackStateCompat.RepeatMode int mRepeatMode;
+        boolean mShuffleModeEnabled;
 
         public MediaSessionImplApi21(Context context, String tag) {
             mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
@@ -2420,6 +2582,38 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            if (mRepeatMode != repeatMode) {
+                mRepeatMode = repeatMode;
+                int size = mExtraControllerCallbacks.beginBroadcast();
+                for (int i = size - 1; i >= 0; i--) {
+                    IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+                    try {
+                        cb.onRepeatModeChanged(repeatMode);
+                    } catch (RemoteException e) {
+                    }
+                }
+                mExtraControllerCallbacks.finishBroadcast();
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            if (mShuffleModeEnabled != enabled) {
+                mShuffleModeEnabled = enabled;
+                int size = mExtraControllerCallbacks.beginBroadcast();
+                for (int i = size - 1; i >= 0; i--) {
+                    IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+                    try {
+                        cb.onShuffleModeChanged(enabled);
+                    } catch (RemoteException e) {
+                    }
+                }
+                mExtraControllerCallbacks.finishBroadcast();
+            }
+        }
+
+        @Override
         public void setExtras(Bundle extras) {
             MediaSessionCompatApi21.setExtras(mSessionObj, extras);
         }
@@ -2621,6 +2815,18 @@
             }
 
             @Override
+            public void setRepeatMode(int repeatMode) throws RemoteException {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
+            public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
             public void sendCustomAction(String action, Bundle args) throws RemoteException {
                 // Will not be called.
                 throw new AssertionError();
@@ -2662,6 +2868,17 @@
             }
 
             @Override
+            @PlaybackStateCompat.RepeatMode
+            public int getRepeatMode() {
+                return mRepeatMode;
+            }
+
+            @Override
+            public boolean isShuffleModeEnabled() {
+                return mShuffleModeEnabled;
+            }
+
+            @Override
             public boolean isTransportControlEnabled() {
                 // Will not be called.
                 throw new AssertionError();
diff --git a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
index 0321fd7..7b1ad31 100644
--- a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -49,7 +49,8 @@
             ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
             ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
             ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
-            ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI})
+            ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI,
+            ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE_ENABLED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface Actions {}
 
@@ -189,6 +190,20 @@
     public static final long ACTION_PREPARE_FROM_URI = 1 << 17;
 
     /**
+     * Indicates this session supports the set repeat mode command.
+     *
+     * @see Builder#setActions(long)
+     */
+    public static final long ACTION_SET_REPEAT_MODE = 1 << 18;
+
+    /**
+     * Indicates this session supports the set shuffle mode enabled command.
+     *
+     * @see Builder#setActions(long)
+     */
+    public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 1 << 19;
+
+    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -386,6 +401,31 @@
      */
     public static final int ERROR_CODE_END_OF_QUEUE = 11;
 
+    /**
+     * @hide
+     */
+    @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RepeatMode {}
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback will be stopped at the end of the playing media list.
+     */
+    public static final int REPEAT_MODE_NONE = 0;
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback of the current playing media item will be repeated.
+     */
+    public static final int REPEAT_MODE_ONE = 1;
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback of the playing media list will be repeated.
+     */
+    public static final int REPEAT_MODE_ALL = 2;
+
     // KeyEvent constants only available on API 11+
     private static final int KEYCODE_MEDIA_PAUSE = 127;
     private static final int KEYCODE_MEDIA_PLAY = 126;
@@ -583,6 +623,8 @@
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+     * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+     * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
      * </ul>
      */
     @Actions
@@ -1121,6 +1163,8 @@
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+         * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+         * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
          * </ul>
          *
          * @return this
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index b118de4..93ead1e 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -26,7 +26,6 @@
 
     <application android:supportsRtl="true">
         <uses-library android:name="android.test.runner"/>
-        <activity android:name="android.support.v4.media.session.TestActivity" />
         <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
             <intent-filter>
                 <action android:name="android.intent.action.MEDIA_BUTTON" />
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index 99c3447..6a997bf 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -175,9 +175,8 @@
         assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
     }
 
-    // TODO(hdmoon): Uncomment after fixing failing tests. (Fails on API 24, 25)
-    // @Test
-    // @SmallTest
+    @Test
+    @SmallTest
     public void testSubscribeWithOptions() {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
@@ -328,9 +327,8 @@
         }
     }
 
-    // TODO(hdmoon): Uncomment after fixing failing tests. (Fails on API 24, 25)
-    // @Test
-    // @SmallTest
+    @Test
+    @SmallTest
     public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
new file mode 100644
index 0000000..fa639a7
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2016 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.v4.media.session;
+
+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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test {@link MediaControllerCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatTest {
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final String SESSION_TAG = "test-session";
+    private static final String EXTRAS_KEY = "test-key";
+    private static final String EXTRAS_VALUE = "test-val";
+    private static final float DELTA = 1e-4f;
+
+    private final Object mWaitLock = new Object();
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private MediaSessionCompat mSession;
+    private MediaSessionCallback mCallback = new MediaSessionCallback();
+    private MediaControllerCompat mController;
+
+    @Before
+    public void setUp() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession = new MediaSessionCompat(getContext(), SESSION_TAG);
+                mSession.setCallback(mCallback, mHandler);
+                mController = mSession.getController();
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.release();
+    }
+
+    @Test
+    @SmallTest
+    public void testGetPackageName() {
+        assertEquals(getContext().getPackageName(), mController.getPackageName());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetRatingType() {
+        assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+                RatingCompat.RATING_NONE, mController.getRatingType());
+
+        mSession.setRatingType(RatingCompat.RATING_5_STARS);
+        assertEquals(RatingCompat.RATING_5_STARS, mController.getRatingType());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetSessionToken() throws Exception {
+        assertEquals(mSession.getSessionToken(), mController.getSessionToken());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCommand() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            final String command = "test-command";
+            final Bundle extras = new Bundle();
+            extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+            mController.sendCommand(command, extras, new ResultReceiver(null));
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCommandCalled);
+            assertNotNull(mCallback.mCommandCallback);
+            assertEquals(command, mCallback.mCommand);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+        }
+    }
+
+    // TODO: Uncomment after fixing this test. This test causes an Exception on System UI.
+    // @Test
+    // @SmallTest
+    public void testVolumeControl() throws Exception {
+        VolumeProviderCompat vp =
+                new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 11, 5) {
+            @Override
+            public void onSetVolumeTo(int volume) {
+                synchronized (mWaitLock) {
+                    setCurrentVolume(volume);
+                    mWaitLock.notify();
+                }
+            }
+
+            @Override
+            public void onAdjustVolume(int direction) {
+                synchronized (mWaitLock) {
+                    switch (direction) {
+                        case AudioManager.ADJUST_LOWER:
+                            setCurrentVolume(getCurrentVolume() - 1);
+                            break;
+                        case AudioManager.ADJUST_RAISE:
+                            setCurrentVolume(getCurrentVolume() + 1);
+                            break;
+                    }
+                    mWaitLock.notify();
+                }
+            }
+        };
+        mSession.setPlaybackToRemote(vp);
+
+        synchronized (mWaitLock) {
+            // test setVolumeTo
+            mController.setVolumeTo(7, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(7, vp.getCurrentVolume());
+
+            // test adjustVolume
+            mController.adjustVolume(AudioManager.ADJUST_LOWER, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(6, vp.getCurrentVolume());
+
+            mController.adjustVolume(AudioManager.ADJUST_RAISE, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(7, vp.getCurrentVolume());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testTransportControlsAndMediaSessionCallback() throws Exception {
+        MediaControllerCompat.TransportControls controls = mController.getTransportControls();
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            controls.play();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayCalled);
+
+            mCallback.reset();
+            controls.pause();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPauseCalled);
+
+            mCallback.reset();
+            controls.stop();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnStopCalled);
+
+            mCallback.reset();
+            controls.fastForward();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnFastForwardCalled);
+
+            mCallback.reset();
+            controls.rewind();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRewindCalled);
+
+            mCallback.reset();
+            controls.skipToPrevious();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+            mCallback.reset();
+            controls.skipToNext();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToNextCalled);
+
+            mCallback.reset();
+            final long seekPosition = 1000;
+            controls.seekTo(seekPosition);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSeekToCalled);
+            assertEquals(seekPosition, mCallback.mSeekPosition);
+
+            mCallback.reset();
+            final RatingCompat rating =
+                    RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+            controls.setRating(rating);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRatingCalled);
+            assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+            assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+            mCallback.reset();
+            final String mediaId = "test-media-id";
+            final Bundle extras = new Bundle();
+            extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+            controls.playFromMediaId(mediaId, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final String query = "test-query";
+            controls.playFromSearch(query, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final Uri uri = Uri.parse("content://test/popcorn.mod");
+            controls.playFromUri(uri, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final String action = "test-action";
+            controls.sendCustomAction(action, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            mCallback.mOnCustomActionCalled = false;
+            final PlaybackStateCompat.CustomAction customAction =
+                    new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+                            .setExtras(extras)
+                            .build();
+            controls.sendCustomAction(customAction, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final long queueItemId = 1000;
+            controls.skipToQueueItem(queueItemId);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToQueueItemCalled);
+            assertEquals(queueItemId, mCallback.mQueueItemId);
+
+            mCallback.reset();
+            controls.prepare();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareCalled);
+
+            mCallback.reset();
+            controls.prepareFromMediaId(mediaId, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            controls.prepareFromSearch(query, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            controls.prepareFromUri(uri, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            controls.setRepeatMode(repeatMode);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRepeatModeCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+
+            mCallback.reset();
+            final boolean shuffleModeEnabled = true;
+            controls.setShuffleModeEnabled(shuffleModeEnabled);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetShuffleModeEnabledCalled);
+            assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testPlaybackInfo() {
+        final int playbackType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
+        final int volumeControl = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        final int maxVolume = 10;
+        final int currentVolume = 3;
+
+        int audioStream = 77;
+        MediaControllerCompat.PlaybackInfo info = new MediaControllerCompat.PlaybackInfo(
+                playbackType, audioStream, volumeControl, maxVolume, currentVolume);
+
+        assertEquals(playbackType, info.getPlaybackType());
+        assertEquals(audioStream, info.getAudioStream());
+        assertEquals(volumeControl, info.getVolumeControl());
+        assertEquals(maxVolume, info.getMaxVolume());
+        assertEquals(currentVolume, info.getCurrentVolume());
+    }
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private long mSeekPosition;
+        private long mQueueItemId;
+        private RatingCompat mRating;
+        private String mMediaId;
+        private String mQuery;
+        private Uri mUri;
+        private String mAction;
+        private String mCommand;
+        private Bundle mExtras;
+        private ResultReceiver mCommandCallback;
+        private int mRepeatMode;
+        private boolean mShuffleModeEnabled;
+
+        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 mOnSkipToQueueItemCalled;
+        private boolean mOnSetRatingCalled;
+        private boolean mOnPlayFromMediaIdCalled;
+        private boolean mOnPlayFromSearchCalled;
+        private boolean mOnPlayFromUriCalled;
+        private boolean mOnCustomActionCalled;
+        private boolean mOnCommandCalled;
+        private boolean mOnPrepareCalled;
+        private boolean mOnPrepareFromMediaIdCalled;
+        private boolean mOnPrepareFromSearchCalled;
+        private boolean mOnPrepareFromUriCalled;
+        private boolean mOnSetRepeatModeCalled;
+        private boolean mOnSetShuffleModeEnabledCalled;
+
+        public void reset() {
+            mSeekPosition = -1;
+            mQueueItemId = -1;
+            mRating = null;
+            mMediaId = null;
+            mQuery = null;
+            mUri = null;
+            mAction = null;
+            mExtras = null;
+            mCommand = null;
+            mCommandCallback = null;
+            mShuffleModeEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
+            mOnSkipToQueueItemCalled = false;
+            mOnSeekToCalled = false;
+            mOnSetRatingCalled = false;
+            mOnPlayFromMediaIdCalled = false;
+            mOnPlayFromSearchCalled = false;
+            mOnPlayFromUriCalled = false;
+            mOnCustomActionCalled = false;
+            mOnCommandCalled = false;
+            mOnPrepareCalled = false;
+            mOnPrepareFromMediaIdCalled = false;
+            mOnPrepareFromSearchCalled = false;
+            mOnPrepareFromUriCalled = false;
+            mOnSetRepeatModeCalled = false;
+            mOnSetShuffleModeEnabledCalled = 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 onSetRating(RatingCompat rating) {
+            synchronized (mWaitLock) {
+                mOnSetRatingCalled = true;
+                mRating = rating;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnCustomActionCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToQueueItem(long id) {
+            synchronized (mWaitLock) {
+                mOnSkipToQueueItemCalled = true;
+                mQueueItemId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+            synchronized (mWaitLock) {
+                mOnCommandCalled = true;
+                mCommand = command;
+                mExtras = extras;
+                mCommandCallback = cb;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepare() {
+            synchronized (mWaitLock) {
+                mOnPrepareCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRepeatMode(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnSetRepeatModeCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetShuffleModeEnabled(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnSetShuffleModeEnabledCalled = true;
+                mShuffleModeEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
index 72460f5..a5f11ed 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -16,85 +16,701 @@
 
 package android.support.v4.media.session;
 
-import static junit.framework.Assert.fail;
+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.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.view.KeyEvent;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.List;
 
+/**
+ * Test {@link MediaSessionCompat}.
+ */
 @RunWith(AndroidJUnit4.class)
 public class MediaSessionCompatTest {
-    @Rule
-    public ActivityTestRule<TestActivity> mActivityRule =
-            new ActivityTestRule<>(TestActivity.class);
-    Context mContext;
-    Map<String, LockedObject> results = new HashMap<>();
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+    private static final String TEST_SESSION_TAG = "test-session-tag";
+    private static final String TEST_KEY = "test-key";
+    private static final String TEST_VALUE = "test-val";
+    private static final String TEST_SESSION_EVENT = "test-session-event";
+    private static final int TEST_CURRENT_VOLUME = 10;
+    private static final int TEST_MAX_VOLUME = 11;
+    private static final long TEST_QUEUE_ID = 12L;
+    private static final long TEST_ACTION = 55L;
+
+    private AudioManager mAudioManager;
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private Object mWaitLock = new Object();
+    private MediaControllerCallback mCallback = new MediaControllerCallback();
+    private MediaSessionCompat mSession;
 
     @Before
-    public void setUp() {
-        mContext = InstrumentationRegistry.getContext();
+    public void setUp() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+                mSession = new MediaSessionCompat(getContext(), TEST_SESSION_TAG);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // It is OK to call release() twice.
+        mSession.release();
+    }
+
+    /**
+     * Tests that a session can be created and that all the fields are
+     * initialized correctly.
+     */
+    @Test
+    @SmallTest
+    public void testCreateSession() throws Exception {
+        assertNotNull(mSession.getSessionToken());
+        assertFalse("New session should not be active", mSession.isActive());
+
+        // Verify by getting the controller and checking all its fields
+        MediaControllerCompat controller = mSession.getController();
+        assertNotNull(controller);
+        verifyNewSession(controller, TEST_SESSION_TAG);
+    }
+
+    /**
+     * Tests MediaSessionCompat.Token created in the constructor of MediaSessionCompat.
+     */
+    @Test
+    @SmallTest
+    public void testSessionToken() throws Exception {
+        MediaSessionCompat.Token sessionToken = mSession.getSessionToken();
+
+        assertNotNull(sessionToken);
+        assertEquals(0, sessionToken.describeContents());
+
+        // Test writeToParcel
+        Parcel p = Parcel.obtain();
+        sessionToken.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        MediaSessionCompat.Token token = MediaSessionCompat.Token.CREATOR.createFromParcel(p);
+        assertEquals(token, sessionToken);
+        p.recycle();
+    }
+
+    /**
+     * Tests that the various configuration bits on a session get passed to the
+     * controller.
+     */
+    @Test
+    @SmallTest
+    public void testConfigureSession() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+
+        synchronized (mWaitLock) {
+            // test setExtras
+            mCallback.resetLocked();
+            final Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            mSession.setExtras(extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnExtraChangedCalled);
+
+            Bundle extrasOut = mCallback.mExtras;
+            assertNotNull(extrasOut);
+            assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+
+            extrasOut = controller.getExtras();
+            assertNotNull(extrasOut);
+            assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+
+            // test setFlags
+            mSession.setFlags(5);
+            assertEquals(5, controller.getFlags());
+
+            // test setMetadata
+            mCallback.resetLocked();
+            MediaMetadataCompat metadata =
+                    new MediaMetadataCompat.Builder().putString(TEST_KEY, TEST_VALUE).build();
+            mSession.setMetadata(metadata);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            metadataOut = controller.getMetadata();
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            // test setPlaybackState
+            mCallback.resetLocked();
+            PlaybackStateCompat state =
+                    new PlaybackStateCompat.Builder().setActions(TEST_ACTION).build();
+            mSession.setPlaybackState(state);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlaybackStateChangedCalled);
+
+            PlaybackStateCompat stateOut = mCallback.mPlaybackState;
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+
+            stateOut = controller.getPlaybackState();
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+
+            // test setQueue and setQueueTitle
+            mCallback.resetLocked();
+            List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
+            MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+                    new MediaDescriptionCompat.Builder()
+                            .setMediaId(TEST_VALUE)
+                            .setTitle("title")
+                            .build(),
+                    TEST_QUEUE_ID);
+            queue.add(item);
+            mSession.setQueue(queue);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueChangedCalled);
+
+            mSession.setQueueTitle(TEST_VALUE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+            assertEquals(TEST_VALUE, mCallback.mTitle);
+            assertEquals(queue.size(), mCallback.mQueue.size());
+            assertEquals(TEST_QUEUE_ID, mCallback.mQueue.get(0).getQueueId());
+            assertEquals(TEST_VALUE, mCallback.mQueue.get(0).getDescription().getMediaId());
+
+            assertEquals(TEST_VALUE, controller.getQueueTitle());
+            assertEquals(queue.size(), controller.getQueue().size());
+            assertEquals(TEST_QUEUE_ID, controller.getQueue().get(0).getQueueId());
+            assertEquals(TEST_VALUE, controller.getQueue().get(0).getDescription().getMediaId());
+
+            mCallback.resetLocked();
+            mSession.setQueue(null);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueChangedCalled);
+
+            mSession.setQueueTitle(null);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+            assertNull(mCallback.mTitle);
+            assertNull(mCallback.mQueue);
+            assertNull(controller.getQueueTitle());
+            assertNull(controller.getQueue());
+
+            // test setSessionActivity
+            Intent intent = new Intent("cts.MEDIA_SESSION_ACTION");
+            PendingIntent pi = PendingIntent.getActivity(getContext(), 555, intent, 0);
+            mSession.setSessionActivity(pi);
+            assertEquals(pi, controller.getSessionActivity());
+
+            // test setRepeatMode
+            mCallback.resetLocked();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            mSession.setRepeatMode(repeatMode);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRepeatModeChangedCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+            assertEquals(repeatMode, controller.getRepeatMode());
+
+            // test setShuffleModeEnabled
+            mCallback.resetLocked();
+            final boolean shuffleModeEnabled = true;
+            mSession.setShuffleModeEnabled(shuffleModeEnabled);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnShuffleModeChangedCalled);
+            assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+            assertEquals(shuffleModeEnabled, controller.isShuffleModeEnabled());
+
+            // test setActivity
+            mSession.setActive(true);
+            assertTrue(mSession.isActive());
+
+            // test sendSessionEvent
+            mCallback.resetLocked();
+            mSession.sendSessionEvent(TEST_SESSION_EVENT, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCallback.mOnSessionEventCalled);
+            assertEquals(TEST_SESSION_EVENT, mCallback.mEvent);
+            assertEquals(TEST_VALUE, mCallback.mExtras.getString(TEST_KEY));
+
+            // test release
+            mCallback.resetLocked();
+            mSession.release();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSessionDestroyedCalled);
+        }
+    }
+
+    /**
+     * Test {@link MediaSessionCompat#setPlaybackToLocal} and
+     * {@link MediaSessionCompat#setPlaybackToRemote}.
+     */
+    @Test
+    @SmallTest
+    public void testPlaybackToLocalAndRemote() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+
+        synchronized (mWaitLock) {
+            // test setPlaybackToRemote, do this before testing setPlaybackToLocal
+            // to ensure it switches correctly.
+            mCallback.resetLocked();
+            try {
+                mSession.setPlaybackToRemote(null);
+                fail("Expected IAE for setPlaybackToRemote(null)");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            VolumeProviderCompat vp = new VolumeProviderCompat(
+                    VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    TEST_MAX_VOLUME,
+                    TEST_CURRENT_VOLUME) {};
+            mSession.setPlaybackToRemote(vp);
+
+            MediaControllerCompat.PlaybackInfo info = null;
+            for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+                mCallback.mOnAudioInfoChangedCalled = false;
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mCallback.mOnAudioInfoChangedCalled);
+                info = mCallback.mPlaybackInfo;
+                if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+                        && info.getMaxVolume() == TEST_MAX_VOLUME
+                        && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+                        && info.getPlaybackType()
+                        == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                    break;
+                }
+            }
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    info.getVolumeControl());
+
+            info = controller.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+            // test setPlaybackToLocal
+            mSession.setPlaybackToLocal(AudioManager.STREAM_RING);
+            info = controller.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                    info.getPlaybackType());
+        }
+    }
+
+    /**
+     * Test {@link MediaSessionCompat.Callback#onMediaButtonEvent}.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackOnMediaButtonEvent() throws Exception {
+        MediaSessionCallback sessionCallback = new MediaSessionCallback();
+        mSession.setCallback(sessionCallback, new Handler(Looper.getMainLooper()));
+        mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
+        mSession.setActive(true);
+
+        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(
+                new ComponentName(getContext(), getContext().getClass()));
+        PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, mediaButtonIntent, 0);
+        mSession.setMediaButtonReceiver(pi);
+
+        long supportedActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
+                | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP
+                | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+                | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+
+        // Set state to STATE_PLAYING to get higher priority.
+        PlaybackStateCompat defaultState = new PlaybackStateCompat.Builder()
+                .setActions(supportedActions)
+                .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f)
+                .build();
+        mSession.setPlaybackState(defaultState);
+
+        synchronized (mWaitLock) {
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPlayCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPauseCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_NEXT);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnSkipToNextCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnSkipToPreviousCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_STOP);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnStopCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnFastForwardCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_REWIND);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnRewindCalled);
+
+            // Test PLAY_PAUSE button twice.
+            // First, send PLAY_PAUSE button event while in STATE_PAUSED.
+            sessionCallback.reset();
+            mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+                    .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPlayCalled);
+
+            // Next, send PLAY_PAUSE button event while in STATE_PLAYING.
+            sessionCallback.reset();
+            mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+                    .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f).build());
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPauseCalled);
+        }
     }
 
     @Test
+    @SmallTest
     public void testSetNullCallback() throws Throwable {
-        initWait("testSetNullCallback");
-        mActivityRule.runOnUiThread(new Runnable() {
+        getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
                 try {
-                    MediaSessionCompat session = new MediaSessionCompat(mContext, "TEST");
+                    MediaSessionCompat session = new MediaSessionCompat(getContext(), "TEST");
                     session.setCallback(null);
                 } catch (Exception e) {
                     fail("Fail with an exception: " + e);
-                } finally {
-                    setResultData("testSetNullCallback", true);
                 }
             }
         });
-        waitFor("testSetNullCallback");
     }
 
-    private void initWait(String key) throws InterruptedException {
-        results.put(key, new LockedObject());
+    /**
+     * Tests {@link MediaSessionCompat.QueueItem}.
+     */
+    @Test
+    @SmallTest
+    public void testQueueItem() {
+        MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+                new MediaDescriptionCompat.Builder()
+                        .setMediaId("media-id")
+                        .setTitle("title")
+                        .build(),
+                TEST_QUEUE_ID);
+        assertEquals(TEST_QUEUE_ID, item.getQueueId());
+        assertEquals("media-id", item.getDescription().getMediaId());
+        assertEquals("title", item.getDescription().getTitle());
+        assertEquals(0, item.describeContents());
+
+        Parcel p = Parcel.obtain();
+        item.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        MediaSessionCompat.QueueItem other =
+                MediaSessionCompat.QueueItem.CREATOR.createFromParcel(p);
+        assertEquals(item.toString(), other.toString());
+        p.recycle();
     }
 
-    private Object[] waitFor(String key) throws InterruptedException {
-        return results.get(key).waitFor();
+    /**
+     * Verifies that a new session hasn't had any configuration bits set yet.
+     *
+     * @param controller The controller for the session
+     */
+    private void verifyNewSession(MediaControllerCompat controller, String tag) {
+        assertEquals("New session has unexpected configuration", 0L, controller.getFlags());
+        assertNull("New session has unexpected configuration", controller.getExtras());
+        assertNull("New session has unexpected configuration", controller.getMetadata());
+        assertEquals("New session has unexpected configuration",
+                getContext().getPackageName(), controller.getPackageName());
+        assertNull("New session has unexpected configuration", controller.getPlaybackState());
+        assertNull("New session has unexpected configuration", controller.getQueue());
+        assertNull("New session has unexpected configuration", controller.getQueueTitle());
+        assertEquals("New session has unexpected configuration", RatingCompat.RATING_NONE,
+                controller.getRatingType());
+        assertNull("New session has unexpected configuration", controller.getSessionActivity());
+
+        assertNotNull(controller.getSessionToken());
+        assertNotNull(controller.getTransportControls());
+
+        MediaControllerCompat.PlaybackInfo info = controller.getPlaybackInfo();
+        assertNotNull(info);
+        assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                info.getPlaybackType());
+        assertEquals(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
+                info.getCurrentVolume());
     }
 
-    private void setResultData(String key, Object... args) {
-        if (results.containsKey(key)) {
-            results.get(key).set(args);
+    private void sendMediaKeyInputToController(int keyCode) {
+        MediaControllerCompat controller = mSession.getController();
+        controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+        controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+    }
+
+    private class MediaControllerCallback extends MediaControllerCompat.Callback {
+        private volatile boolean mOnPlaybackStateChangedCalled;
+        private volatile boolean mOnMetadataChangedCalled;
+        private volatile boolean mOnQueueChangedCalled;
+        private volatile boolean mOnQueueTitleChangedCalled;
+        private volatile boolean mOnExtraChangedCalled;
+        private volatile boolean mOnAudioInfoChangedCalled;
+        private volatile boolean mOnSessionDestroyedCalled;
+        private volatile boolean mOnSessionEventCalled;
+        private volatile boolean mOnRepeatModeChangedCalled;
+        private volatile boolean mOnShuffleModeChangedCalled;
+
+        private volatile PlaybackStateCompat mPlaybackState;
+        private volatile MediaMetadataCompat mMediaMetadata;
+        private volatile List<MediaSessionCompat.QueueItem> mQueue;
+        private volatile CharSequence mTitle;
+        private volatile String mEvent;
+        private volatile Bundle mExtras;
+        private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+        private volatile int mRepeatMode;
+        private volatile boolean mShuffleModeEnabled;
+
+        public void resetLocked() {
+            mOnPlaybackStateChangedCalled = false;
+            mOnMetadataChangedCalled = false;
+            mOnQueueChangedCalled = false;
+            mOnQueueTitleChangedCalled = false;
+            mOnExtraChangedCalled = false;
+            mOnAudioInfoChangedCalled = false;
+            mOnSessionDestroyedCalled = false;
+            mOnSessionEventCalled = false;
+            mOnRepeatModeChangedCalled = false;
+            mOnShuffleModeChangedCalled = false;
+
+            mPlaybackState = null;
+            mMediaMetadata = null;
+            mQueue = null;
+            mTitle = null;
+            mExtras = null;
+            mPlaybackInfo = null;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleModeEnabled = false;
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mWaitLock) {
+                mOnPlaybackStateChangedCalled = true;
+                mPlaybackState = state;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mWaitLock) {
+                mOnMetadataChangedCalled = true;
+                mMediaMetadata = metadata;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueChanged(List<MediaSessionCompat.QueueItem> queue) {
+            synchronized (mWaitLock) {
+                mOnQueueChangedCalled = true;
+                mQueue = queue;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueTitleChanged(CharSequence title) {
+            synchronized (mWaitLock) {
+                mOnQueueTitleChangedCalled = true;
+                mTitle = title;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnExtraChangedCalled = true;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+            synchronized (mWaitLock) {
+                mOnAudioInfoChangedCalled = true;
+                mPlaybackInfo = info;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            synchronized (mWaitLock) {
+                mOnSessionDestroyedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSessionEventCalled = true;
+                mEvent = event;
+                mExtras = (Bundle) extras.clone();
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRepeatModeChanged(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnRepeatModeChangedCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onShuffleModeChanged(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnShuffleModeChangedCalled = true;
+                mShuffleModeEnabled = enabled;
+                mWaitLock.notify();
+            }
         }
     }
 
-    private class LockedObject {
-        private Semaphore mLock = new Semaphore(1);
-        private volatile Object[] mArgs;
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private boolean mOnPlayCalled;
+        private boolean mOnPauseCalled;
+        private boolean mOnStopCalled;
+        private boolean mOnFastForwardCalled;
+        private boolean mOnRewindCalled;
+        private boolean mOnSkipToPreviousCalled;
+        private boolean mOnSkipToNextCalled;
 
-        public LockedObject() {
-            mLock.drainPermits();
+        public void reset() {
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
         }
 
-        public void set(Object... args) {
-            mArgs = args;
-            mLock.release(1);
+        @Override
+        public void onPlay() {
+            synchronized (mWaitLock) {
+                mOnPlayCalled = true;
+                mWaitLock.notify();
+            }
         }
 
-        public Object[] waitFor() throws InterruptedException {
-            mLock.tryAcquire(1, 2, TimeUnit.SECONDS);
-            return mArgs;
+        @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();
+            }
         }
     }
 }
diff --git a/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
new file mode 100644
index 0000000..410ae01
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 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.v4.media.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * Test {@link PlaybackStateCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class PlaybackStateCompatTest {
+
+    private static final long TEST_POSITION = 20000L;
+    private static final long TEST_BUFFERED_POSITION = 15000L;
+    private static final long TEST_UPDATE_TIME = 100000L;
+    private static final long TEST_ACTIONS = PlaybackStateCompat.ACTION_PLAY
+            | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SEEK_TO;
+    private static final long TEST_QUEUE_ITEM_ID = 23L;
+    private static final float TEST_PLAYBACK_SPEED = 3.0f;
+    private static final float TEST_PLAYBACK_SPEED_ON_REWIND = -2.0f;
+    private static final float DELTA = 1e-7f;
+
+    private static final String TEST_ERROR_MSG = "test-error-msg";
+    private static final String TEST_CUSTOM_ACTION = "test-custom-action";
+    private static final String TEST_CUSTOM_ACTION_NAME = "test-custom-action-name";
+    private static final int TEST_ICON_RESOURCE_ID = android.R.drawable.ic_media_next;
+
+    private static final String EXTRAS_KEY = "test-key";
+    private static final String EXTRAS_VALUE = "test-value";
+
+    /**
+     * Test default values of {@link PlaybackStateCompat}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder() {
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder().build();
+
+        assertEquals(new ArrayList<PlaybackStateCompat.CustomAction>(), state.getCustomActions());
+        assertEquals(0, state.getState());
+        assertEquals(0L, state.getPosition());
+        assertEquals(0L, state.getBufferedPosition());
+        assertEquals(0.0f, state.getPlaybackSpeed(), DELTA);
+        assertEquals(0L, state.getActions());
+        assertNull(state.getErrorMessage());
+        assertEquals(0L, state.getLastPositionUpdateTime());
+        assertEquals(MediaSessionCompat.QueueItem.UNKNOWN_ID, state.getActiveQueueItemId());
+        assertNull(state.getExtras());
+    }
+
+    /**
+     * Test following setter methods of {@link PlaybackStateCompat.Builder}:
+     * {@link PlaybackStateCompat.Builder#setState(int, long, float)}
+     * {@link PlaybackStateCompat.Builder#setActions(long)}
+     * {@link PlaybackStateCompat.Builder#setActiveQueueItemId(long)}
+     * {@link PlaybackStateCompat.Builder#setBufferedPosition(long)}
+     * {@link PlaybackStateCompat.Builder#setErrorMessage(CharSequence)}
+     * {@link PlaybackStateCompat.Builder#setExtras(Bundle)}
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_setterMethods() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+                .setState(PlaybackStateCompat.STATE_PLAYING, TEST_POSITION, TEST_PLAYBACK_SPEED)
+                .setActions(TEST_ACTIONS)
+                .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+                .setBufferedPosition(TEST_BUFFERED_POSITION)
+                .setErrorMessage(TEST_ERROR_MSG)
+                .setExtras(extras)
+                .build();
+        assertEquals(PlaybackStateCompat.STATE_PLAYING, state.getState());
+        assertEquals(TEST_POSITION, state.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED, state.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_ACTIONS, state.getActions());
+        assertEquals(TEST_QUEUE_ITEM_ID, state.getActiveQueueItemId());
+        assertEquals(TEST_BUFFERED_POSITION, state.getBufferedPosition());
+        assertEquals(TEST_ERROR_MSG, state.getErrorMessage().toString());
+        assertNotNull(state.getExtras());
+        assertEquals(EXTRAS_VALUE, state.getExtras().get(EXTRAS_KEY));
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#setState(int, long, float, long)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_setStateWithUpdateTime() {
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+                .setState(
+                        PlaybackStateCompat.STATE_REWINDING,
+                        TEST_POSITION,
+                        TEST_PLAYBACK_SPEED_ON_REWIND,
+                        TEST_UPDATE_TIME)
+                .build();
+        assertEquals(PlaybackStateCompat.STATE_REWINDING, state.getState());
+        assertEquals(TEST_POSITION, state.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED_ON_REWIND, state.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_UPDATE_TIME, state.getLastPositionUpdateTime());
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#addCustomAction(String, String, int)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_addCustomAction() {
+        ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+        for (int i = 0; i < 5; i++) {
+            actions.add(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .build());
+            builder.addCustomAction(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i);
+        }
+
+        PlaybackStateCompat state = builder.build();
+        assertEquals(actions.size(), state.getCustomActions().size());
+        for (int i = 0; i < actions.size(); i++) {
+            assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+        }
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_addCustomActionWithCustomActionObject() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+        for (int i = 0; i < 5; i++) {
+            actions.add(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .setExtras(extras)
+                    .build());
+            builder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .setExtras(extras)
+                    .build());
+        }
+
+        PlaybackStateCompat state = builder.build();
+        assertEquals(actions.size(), state.getCustomActions().size());
+        for (int i = 0; i < actions.size(); i++) {
+            assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+        }
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat#writeToParcel(Parcel, int)}.
+     */
+    @Test
+    @SmallTest
+    public void testWriteToParcel() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        PlaybackStateCompat.Builder builder =
+                new PlaybackStateCompat.Builder()
+                        .setState(PlaybackStateCompat.STATE_CONNECTING, TEST_POSITION,
+                                TEST_PLAYBACK_SPEED, TEST_UPDATE_TIME)
+                        .setActions(TEST_ACTIONS)
+                        .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+                        .setBufferedPosition(TEST_BUFFERED_POSITION)
+                        .setErrorMessage(TEST_ERROR_MSG)
+                        .setExtras(extras);
+
+        for (int i = 0; i < 5; i++) {
+            builder.addCustomAction(
+                    new PlaybackStateCompat.CustomAction.Builder(
+                            TEST_CUSTOM_ACTION + i,
+                            TEST_CUSTOM_ACTION_NAME + i,
+                            TEST_ICON_RESOURCE_ID + i)
+                            .setExtras(extras)
+                            .build());
+        }
+        PlaybackStateCompat state = builder.build();
+
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        PlaybackStateCompat stateOut = PlaybackStateCompat.CREATOR.createFromParcel(parcel);
+        assertEquals(PlaybackStateCompat.STATE_CONNECTING, stateOut.getState());
+        assertEquals(TEST_POSITION, stateOut.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED, stateOut.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_UPDATE_TIME, stateOut.getLastPositionUpdateTime());
+        assertEquals(TEST_BUFFERED_POSITION, stateOut.getBufferedPosition());
+        assertEquals(TEST_ACTIONS, stateOut.getActions());
+        assertEquals(TEST_QUEUE_ITEM_ID, stateOut.getActiveQueueItemId());
+        assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage());
+        assertNotNull(stateOut.getExtras());
+        assertEquals(EXTRAS_VALUE, stateOut.getExtras().get(EXTRAS_KEY));
+
+        assertEquals(state.getCustomActions().size(), stateOut.getCustomActions().size());
+        for (int i = 0; i < state.getCustomActions().size(); i++) {
+            assertCustomActionEquals(
+                    state.getCustomActions().get(i), stateOut.getCustomActions().get(i));
+        }
+        parcel.recycle();
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat#describeContents()}.
+     */
+    @Test
+    @SmallTest
+    public void testDescribeContents() {
+        assertEquals(0, new PlaybackStateCompat.Builder().build().describeContents());
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.CustomAction}.
+     */
+    @Test
+    @SmallTest
+    public void testCustomAction() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        // Test Builder/Getters
+        PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction
+                .Builder(TEST_CUSTOM_ACTION, TEST_CUSTOM_ACTION_NAME, TEST_ICON_RESOURCE_ID)
+                .setExtras(extras)
+                .build();
+        assertEquals(TEST_CUSTOM_ACTION, customAction.getAction());
+        assertEquals(TEST_CUSTOM_ACTION_NAME, customAction.getName().toString());
+        assertEquals(TEST_ICON_RESOURCE_ID, customAction.getIcon());
+        assertEquals(EXTRAS_VALUE, customAction.getExtras().get(EXTRAS_KEY));
+
+        // Test describeContents
+        assertEquals(0, customAction.describeContents());
+
+        // Test writeToParcel
+        Parcel parcel = Parcel.obtain();
+        customAction.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        assertCustomActionEquals(
+                customAction, PlaybackStateCompat.CustomAction.CREATOR.createFromParcel(parcel));
+        parcel.recycle();
+    }
+
+    private void assertCustomActionEquals(PlaybackStateCompat.CustomAction action1,
+            PlaybackStateCompat.CustomAction action2) {
+        assertEquals(action1.getAction(), action2.getAction());
+        assertEquals(action1.getName(), action2.getName());
+        assertEquals(action1.getIcon(), action2.getIcon());
+
+        // To be the same, two extras should be both null or both not null.
+        assertEquals(action1.getExtras() != null, action2.getExtras() != null);
+        if (action1.getExtras() != null) {
+            assertEquals(action1.getExtras().get(EXTRAS_KEY), action2.getExtras().get(EXTRAS_KEY));
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
deleted file mode 100644
index dd56467..0000000
--- a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2016 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.v4.media.session;
-
-import android.app.Activity;
-
-public class TestActivity extends Activity {
-}
diff --git a/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java b/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
index 17976a8..b1358d0 100644
--- a/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
+++ b/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
@@ -98,7 +98,7 @@
      * @param editorInfo the editor with which we associate supported MIME types
      * @param contentMimeTypes an array of MIME types. {@code null} and an empty array means that
      *                         {@link InputConnectionCompat#commitContent(
-     *                         InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)
+     *                         InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)}
      *                         is not supported on this Editor
      */
     public static void setContentMimeTypes(@NonNull EditorInfo editorInfo,
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 2b70139..d14dc74 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -47,8 +47,10 @@
                 @Override
                 public void onChildViewHolderSelected(RecyclerView parent,
                         RecyclerView.ViewHolder view, int position, int subposition) {
-                    mSelectedPosition = position;
-                    onRowSelected(parent, view, position, subposition);
+                    if (!mLateSelectionObserver.mIsLateSelection) {
+                        mSelectedPosition = position;
+                        onRowSelected(parent, view, position, subposition);
+                    }
                 }
             };
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
index 0554105..d36f68f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -50,8 +50,10 @@
                 @Override
                 public void onChildViewHolderSelected(RecyclerView parent,
                         RecyclerView.ViewHolder view, int position, int subposition) {
-                    mSelectedPosition = position;
-                    onRowSelected(parent, view, position, subposition);
+                    if (!mLateSelectionObserver.mIsLateSelection) {
+                        mSelectedPosition = position;
+                        onRowSelected(parent, view, position, subposition);
+                    }
                 }
             };
 
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
index 483f3fb..c07f3b0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
@@ -170,7 +170,7 @@
 
     /**
      * Removes an item from the cache. This will force the item to be re-read
-     * from the data source the next time (@link #get(int)} is called.
+     * from the data source the next time {@link #get(int)} is called.
      */
     protected final void invalidateCache(int index) {
         mItemCache.remove(index);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
index d2ee2f3..b307df8 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
@@ -69,8 +69,8 @@
     }
 
     /**
-     * Get a label for value. The label can be static ({@link #setStaticLabels(CharSequence[])}
-     * or dynamically generated (@link {@link #setLabelFormat(String)} when static labels is null.
+     * Get a label for value. The label can be static {@link #setStaticLabels(CharSequence[])}
+     * or dynamically generated {@link #setLabelFormat(String)} when static labels is null.
      * 
      * @param value Value between minValue and maxValue.
      * @return Label for the value.
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
index 476023c..4148ff5 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -17,7 +17,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Rect;
@@ -107,6 +106,12 @@
             new Handler().postDelayed(new Runnable() {
                 @Override
                 public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
                     ListRowPresenter lrp = new ListRowPresenter();
                     ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
                     setAdapter(adapter);
@@ -122,7 +127,6 @@
 
         final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -138,6 +142,12 @@
             new Handler().postDelayed(new Runnable() {
                 @Override
                 public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
                     loadData(adapter, 10, 1);
                 }
             }, 1000);
@@ -150,7 +160,6 @@
 
         final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -178,7 +187,6 @@
 
         final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -220,7 +228,6 @@
         // but we could get Fragment from static variable.
         RowsFragment fragment = sLastF_restoreSelection.get();
         final VerticalGridView gridView = fragment.getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
index f22b961..65ca014 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -20,7 +20,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Rect;
@@ -110,6 +109,12 @@
             new Handler().postDelayed(new Runnable() {
                 @Override
                 public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
                     ListRowPresenter lrp = new ListRowPresenter();
                     ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
                     setAdapter(adapter);
@@ -125,7 +130,6 @@
 
         final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -141,6 +145,12 @@
             new Handler().postDelayed(new Runnable() {
                 @Override
                 public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
                     loadData(adapter, 10, 1);
                 }
             }, 1000);
@@ -153,7 +163,6 @@
 
         final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -181,7 +190,6 @@
 
         final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
                 .getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
     }
@@ -223,7 +231,6 @@
         // but we could get Fragment from static variable.
         RowsSupportFragment fragment = sLastF_restoreSelection.get();
         final VerticalGridView gridView = fragment.getVerticalGridView();
-        assertNull(gridView.findViewHolderForAdapterPosition(0));
         assertEquals(7, gridView.getSelectedPosition());
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
 
diff --git a/v7/appcompat/src/android/support/v7/app/AlertDialog.java b/v7/appcompat/src/android/support/v7/app/AlertDialog.java
index 529a10c..051f5cc 100644
--- a/v7/appcompat/src/android/support/v7/app/AlertDialog.java
+++ b/v7/appcompat/src/android/support/v7/app/AlertDialog.java
@@ -145,9 +145,9 @@
     }
 
     /**
-     * @see Builder#setCustomTitle(View)
-     *
      * This method has no effect if called after {@link #show()}.
+     *
+     * @see Builder#setCustomTitle(View)
      */
     public void setCustomTitle(View customTitleView) {
         mAlert.setCustomTitle(customTitleView);
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index bd72879..a744c5f 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -656,7 +656,7 @@
      * Notification noti = new NotificationCompat.Builder()
      *     .setSmallIcon(R.drawable.ic_stat_player)
      *     .setLargeIcon(albumArtBitmap))
-     *     .setCustomContentView(contentView);
+     *     .setCustomContentView(contentView)
      *     .setStyle(<b>new NotificationCompat.DecoratedCustomViewStyle()</b>)
      *     .build();
      * </pre>
@@ -690,7 +690,7 @@
      * Notification noti = new Notification.Builder()
      *     .setSmallIcon(R.drawable.ic_stat_player)
      *     .setLargeIcon(albumArtBitmap))
-     *     .setCustomContentView(contentView);
+     *     .setCustomContentView(contentView)
      *     .setStyle(<b>new NotificationCompat.DecoratedMediaCustomViewStyle()</b>
      *          .setMediaSession(mySession))
      *     .build();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
index 44c05ae..34c07a3 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
@@ -43,6 +43,8 @@
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
 import android.support.v7.app.BaseInstrumentationTestCase;
@@ -125,8 +127,9 @@
                 .check(matches(isDisplayed()));
     }
 
+    @FlakyTest(bugId = 33669575)
     @Test
-    @SmallTest
+    @LargeTest
     public void testAnchoring() {
         Builder popupBuilder = new Builder();
         popupBuilder.wireToActionButton();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
index 3cf511f..7d6e39f 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
@@ -48,6 +48,8 @@
 import android.support.test.espresso.Root;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.v7.app.BaseInstrumentationTestCase;
 import android.support.v7.appcompat.test.R;
@@ -260,8 +262,9 @@
         };
     }
 
+    @FlakyTest(bugId = 33669575)
     @Test
-    @MediumTest
+    @LargeTest
     public void testAnchoring() {
         Builder menuBuilder = new Builder();
         menuBuilder.wireToActionButton();
diff --git a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
index 7ade21d..2613f90 100644
--- a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
+++ b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
@@ -19,6 +19,7 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
 import android.os.Build;
 import android.support.annotation.RequiresApi;
 import android.util.Log;
@@ -34,6 +35,11 @@
 final class MediaRouterJellybean {
     private static final String TAG = "MediaRouterJellybean";
 
+    // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+    // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100;
+    // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200;
+    public static final int DEVICE_OUT_BLUETOOTH = 0x80 | 0x100 | 0x200;
+
     public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
     public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
     public static final int ROUTE_TYPE_USER = 0x00800000;
@@ -116,17 +122,15 @@
         return new VolumeCallbackProxy<VolumeCallback>(callback);
     }
 
-    static boolean isBluetoothA2dpOn(Object routerObj) {
+    static boolean checkRoutedToBluetooth(Context context) {
         try {
-            Field globalRouterField = routerObj.getClass().getDeclaredField("sStatic");
-            globalRouterField.setAccessible(true);
-            Object globalRouterObj = globalRouterField.get(null);
-            Method method = globalRouterObj.getClass().getDeclaredMethod("isBluetoothA2dpOn");
-            method.setAccessible(true);
-            Object result = method.invoke(globalRouterObj);
-            return (Boolean) result;
-        } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
-                | NoSuchMethodException | InvocationTargetException e) {
+            AudioManager audioManager = (AudioManager) context.getSystemService(
+                    Context.AUDIO_SERVICE);
+            Method method = audioManager.getClass().getDeclaredMethod(
+                    "getDevicesForStream", int.class);
+            int device = (Integer) method.invoke(audioManager, AudioManager.STREAM_MUSIC);
+            return (device & DEVICE_OUT_BLUETOOTH) != 0;
+        } catch (Exception e) {
             return false;
         }
     }
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index d398e15..3c8e64d 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -18,6 +18,7 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -94,7 +95,7 @@
      * the disconnect button to disconnect and keep playing.
      * <p>
      *
-     * @see {@link MediaRouteDescriptor#canDisconnectAndKeepPlaying()}.
+     * @see MediaRouteDescriptor#canDisconnectAndKeepPlaying()
      */
     public static final int UNSELECT_REASON_DISCONNECTED = 1;
     /**
@@ -2249,30 +2250,20 @@
             }
         }
 
+        @TargetApi(16)
         void syncSystemRoutes() {
             Object routerObj = MediaRouterJellybean.getMediaRouter(mApplicationContext);
-            // If a2dp is enabled, this means a BT route is the selected route, otherwise
-            // the default route is the selected one.
-            boolean a2dpEnabled = MediaRouterJellybean.isBluetoothA2dpOn(routerObj);
+            boolean routedToBluetooth = MediaRouterJellybean.checkRoutedToBluetooth(
+                mApplicationContext);
             Object selectedRouteObj = MediaRouterJellybean.getSelectedRoute(
                     routerObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
             Object defaultRouteObj = mSystemProvider.getDefaultRoute();
+            Object bluetoothRouteObj = mSystemProvider.getSystemRoute(mBluetoothRoute);
 
-            if (a2dpEnabled && selectedRouteObj == defaultRouteObj) {
-                // A BT route is the currently selected route, but MediaRouter think the default
-                // route is the selected one. By selecting the BT route via framework MediaRouter,
-                // MediaRouter could correct its selected route information.
-                for (Object routeObj : MediaRouterJellybean.getRoutes(routerObj)) {
-                    if (routeObj != defaultRouteObj) {
-                        MediaRouterJellybean.selectRoute(routerObj,
-                                MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
-                        break;
-                    }
-                }
-            } else if (!a2dpEnabled && selectedRouteObj != defaultRouteObj) {
-                // The default route is the currently selected route, but MediaRouter think a BT
-                // route is the selected one. By selecting the default route via framework
-                // MediaRouter, MediaRouter could correct its selected route information.
+            if (routedToBluetooth && selectedRouteObj == defaultRouteObj) {
+                MediaRouterJellybean.selectRoute(routerObj,
+                    MediaRouterJellybean.ALL_ROUTE_TYPES, bluetoothRouteObj);
+            } else if (!routedToBluetooth && selectedRouteObj == bluetoothRouteObj) {
                 MediaRouterJellybean.selectRoute(routerObj,
                         MediaRouterJellybean.ALL_ROUTE_TYPES, defaultRouteObj);
             }
@@ -2507,7 +2498,7 @@
             }
             if (mBluetoothRoute == null && !mRoutes.isEmpty()) {
                 for (RouteInfo route : mRoutes) {
-                    if (isSystemBluetoothRoute(route) && isRouteSelectable(route)) {
+                    if (isSystemLiveAudioOnlyRoute(route) && isRouteSelectable(route)) {
                         mBluetoothRoute = route;
                         Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute);
                         break;
@@ -2599,12 +2590,6 @@
                             SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
         }
 
-        private boolean isSystemBluetoothRoute(RouteInfo route) {
-            return route.getProviderInstance() == mSystemProvider
-                    && !route.mDescriptorId.equals(
-                            SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
-        }
-
         private void setSelectedRouteInternal(RouteInfo route, int unselectReason) {
             if (mSelectedRoute != route) {
                 if (mSelectedRoute != null) {
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index 5fcafa7..0833be3 100644
--- a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
@@ -103,6 +103,10 @@
         return null;
     }
 
+    protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+        return null;
+    }
+
     /**
      * Legacy implementation for platform versions prior to Jellybean.
      */
@@ -664,6 +668,18 @@
             return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
         }
 
+        @Override
+        protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+            if (route == null) {
+                return null;
+            }
+            int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+            if (index >= 0) {
+                return mSystemRouteRecords.get(index).mRouteObj;
+            }
+            return null;
+        }
+
         /**
          * Represents a route that is provided by the framework media router
          * and published by this route provider to the support library media router.
diff --git a/v7/recyclerview/src/android/support/v7/util/SortedList.java b/v7/recyclerview/src/android/support/v7/util/SortedList.java
index 74cf1fe..f96433f 100644
--- a/v7/recyclerview/src/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/android/support/v7/util/SortedList.java
@@ -126,9 +126,9 @@
      * @param item The item to be added into the list.
      *
      * @return The index of the newly added item.
-     * @see {@link Callback#compare(Object, Object)}
-     * @see {@link Callback#areItemsTheSame(Object, Object)}
-     * @see {@link Callback#areContentsTheSame(Object, Object)}}
+     * @see Callback#compare(Object, Object)
+     * @see Callback#areItemsTheSame(Object, Object)
+     * @see Callback#areContentsTheSame(Object, Object)}
      */
     public int add(T item) {
         throwIfMerging();
@@ -145,7 +145,7 @@
      * </p>
      * @param items Array of items to be added into the list.
      * @param mayModifyInput If true, SortedList is allowed to modify the input.
-     * @see {@link SortedList#addAll(Object[] items)}.
+     * @see SortedList#addAll(Object[] items)
      */
     public void addAll(T[] items, boolean mayModifyInput) {
         throwIfMerging();
@@ -165,7 +165,7 @@
     /**
      * Adds the given items to the list. Does not modify the input.
      *
-     * @see {@link SortedList#addAll(T[] items, boolean mayModifyInput)}
+     * @see SortedList#addAll(T[] items, boolean mayModifyInput)
      *
      * @param items Array of items to be added into the list.
      */
@@ -176,7 +176,7 @@
     /**
      * Adds the given items to the list. Does not modify the input.
      *
-     * @see {@link SortedList#addAll(T[] items, boolean mayModifyInput)}
+     * @see SortedList#addAll(T[] items, boolean mayModifyInput)
      *
      * @param items Collection of items to be added into the list.
      */
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 24936e0..479d684 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1339,7 +1339,7 @@
      *
      * @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
      *
-     * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
+     * @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)
      */
     public void setViewCacheExtension(ViewCacheExtension extension) {
         mRecycler.setViewCacheExtension(extension);