Merge "Add PlaybackControlGlue for support v4 fragment" into mnc-ub-dev
diff --git a/design/build.gradle b/design/build.gradle
index e785f83..af46827 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -39,3 +39,70 @@
         consumerProguardFiles 'proguard-rules.pro'
     }
 }
+
+android.libraryVariants.all { variant ->
+    def name = variant.buildType.name
+
+    if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
+        return; // Skip debug builds.
+    }
+    def suffix = name.capitalize()
+
+    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+        dependsOn variant.javaCompile
+        from variant.javaCompile.destinationDir
+        from 'LICENSE.txt'
+    }
+    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+        source android.sourceSets.main.java
+        classpath = files(variant.javaCompile.classpath.files) + files(
+                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+    }
+
+    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+        classifier = 'javadoc'
+        from 'build/docs/javadoc'
+    }
+
+    def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+        classifier = 'sources'
+        from android.sourceSets.main.java.srcDirs
+    }
+
+    artifacts.add('archives', javadocJarTask);
+    artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+    repositories {
+        mavenDeployer {
+            repository(url: uri(rootProject.ext.supportRepoOut)) {
+            }
+
+            pom.project {
+                name 'Android Design Support Library'
+                description "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 4 or later."
+                url 'http://developer.android.com/tools/extras/support-library.html'
+                inceptionYear '2011'
+
+                licenses {
+                    license {
+                        name 'The Apache Software License, Version 2.0'
+                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                        distribution 'repo'
+                    }
+                }
+
+                scm {
+                    url "http://source.android.com"
+                    connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+                }
+                developers {
+                    developer {
+                        name 'The Android Open Source Project'
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/v17/preference-leanback/res/layout/leanback_preference.xml b/v17/preference-leanback/res/layout/leanback_preference.xml
index 498b1073..05ca21c 100644
--- a/v17/preference-leanback/res/layout/leanback_preference.xml
+++ b/v17/preference-leanback/res/layout/leanback_preference.xml
@@ -34,8 +34,8 @@
         android:layout_gravity="center_vertical" >
         <ImageView
             android:id="@android:id/icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_width="@dimen/lb_preference_item_icon_size"
+            android:layout_height="@dimen/lb_preference_item_icon_size"
             android:layout_marginEnd="@dimen/lb_preference_item_icon_margin_end"
             />
     </FrameLayout>
diff --git a/v4/java/android/support/v4/media/MediaBrowserCompat.java b/v4/java/android/support/v4/media/MediaBrowserCompat.java
index b4559b0..6e2c9ad 100644
--- a/v4/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/v4/java/android/support/v4/media/MediaBrowserCompat.java
@@ -52,29 +52,7 @@
  * @hide
  */
 public final class MediaBrowserCompat {
-    private static final String TAG = "MediaBrowserCompat";
-    private static final boolean DBG = false;
-
-    private static final int CONNECT_STATE_DISCONNECTED = 0;
-    private static final int CONNECT_STATE_CONNECTING = 1;
-    private static final int CONNECT_STATE_CONNECTED = 2;
-    private static final int CONNECT_STATE_SUSPENDED = 3;
-
-    private final Context mContext;
-    private final ComponentName mServiceComponent;
-    private final ConnectionCallback mCallback;
-    private final Bundle mRootHints;
-    private final Handler mHandler = new Handler();
-    private final ArrayMap<String,Subscription> mSubscriptions =
-            new ArrayMap<String, MediaBrowserCompat.Subscription>();
-
-    private int mState = CONNECT_STATE_DISCONNECTED;
-    private MediaServiceConnection mServiceConnection;
-    private IMediaBrowserServiceCompat mServiceBinder;
-    private IMediaBrowserServiceCompatCallbacks mServiceCallbacks;
-    private String mRootId;
-    private MediaSessionCompat.Token mMediaSessionToken;
-    private Bundle mExtras;
+    private final MediaBrowserImplBase mImpl;
 
     /**
      * Creates a media browser for the specified media browse service.
@@ -89,19 +67,7 @@
      */
     public MediaBrowserCompat(Context context, ComponentName serviceComponent,
             ConnectionCallback callback, Bundle rootHints) {
-        if (context == null) {
-            throw new IllegalArgumentException("context must not be null");
-        }
-        if (serviceComponent == null) {
-            throw new IllegalArgumentException("service component must not be null");
-        }
-        if (callback == null) {
-            throw new IllegalArgumentException("connection callback must not be null");
-        }
-        mContext = context;
-        mServiceComponent = serviceComponent;
-        mCallback = callback;
-        mRootHints = rootHints;
+        mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
     }
 
     /**
@@ -112,62 +78,7 @@
      * </p>
      */
     public void connect() {
-        if (mState != CONNECT_STATE_DISCONNECTED) {
-            throw new IllegalStateException("connect() called while not disconnected (state="
-                    + getStateLabel(mState) + ")");
-        }
-        // TODO: remove this extra check.
-        if (DBG) {
-            if (mServiceConnection != null) {
-                throw new RuntimeException("mServiceConnection should be null. Instead it is "
-                        + mServiceConnection);
-            }
-        }
-        if (mServiceBinder != null) {
-            throw new RuntimeException("mServiceBinder should be null. Instead it is "
-                    + mServiceBinder);
-        }
-        if (mServiceCallbacks != null) {
-            throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
-                    + mServiceCallbacks);
-        }
-
-        mState = CONNECT_STATE_CONNECTING;
-
-        final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
-        intent.setComponent(mServiceComponent);
-
-        final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
-
-        boolean bound = false;
-        try {
-            bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
-        } catch (Exception ex) {
-            Log.e(TAG, "Failed binding to service " + mServiceComponent);
-        }
-
-        if (!bound) {
-            // Tell them that it didn't work.  We are already on the main thread,
-            // but we don't want to do callbacks inside of connect().  So post it,
-            // and then check that we are on the same ServiceConnection.  We know
-            // we won't also get an onServiceConnected or onServiceDisconnected,
-            // so we won't be doing double callbacks.
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    // Ensure that nobody else came in or tried to connect again.
-                    if (thisConnection == mServiceConnection) {
-                        forceCloseConnection();
-                        mCallback.onConnectionFailed();
-                    }
-                }
-            });
-        }
-
-        if (DBG) {
-            Log.d(TAG, "connect...");
-            dump();
-        }
+        mImpl.connect();
     }
 
     /**
@@ -175,53 +86,14 @@
      * After this, no more callbacks will be received.
      */
     public void disconnect() {
-        // It's ok to call this any state, because allowing this lets apps not have
-        // to check isConnected() unnecessarily.  They won't appreciate the extra
-        // assertions for this.  We do everything we can here to go back to a sane state.
-        if (mServiceCallbacks != null) {
-            try {
-                mServiceBinder.disconnect(mServiceCallbacks);
-            } catch (RemoteException ex) {
-                // We are disconnecting anyway.  Log, just for posterity but it's not
-                // a big problem.
-                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
-            }
-        }
-        forceCloseConnection();
-
-        if (DBG) {
-            Log.d(TAG, "disconnect...");
-            dump();
-        }
-    }
-
-    /**
-     * Null out the variables and unbind from the service.  This doesn't include
-     * calling disconnect on the service, because we only try to do that in the
-     * clean shutdown cases.
-     * <p>
-     * Everywhere that calls this EXCEPT for disconnect() should follow it with
-     * a call to mCallback.onConnectionFailed().  Disconnect doesn't do that callback
-     * for a clean shutdown, but everywhere else is a dirty shutdown and should
-     * notify the app.
-     */
-    private void forceCloseConnection() {
-        if (mServiceConnection != null) {
-            mContext.unbindService(mServiceConnection);
-        }
-        mState = CONNECT_STATE_DISCONNECTED;
-        mServiceConnection = null;
-        mServiceBinder = null;
-        mServiceCallbacks = null;
-        mRootId = null;
-        mMediaSessionToken = null;
+        mImpl.disconnect();
     }
 
     /**
      * Returns whether the browser is connected to the service.
      */
     public boolean isConnected() {
-        return mState == CONNECT_STATE_CONNECTED;
+        return mImpl.isConnected();
     }
 
     /**
@@ -229,11 +101,7 @@
      */
     public @NonNull
     ComponentName getServiceComponent() {
-        if (!isConnected()) {
-            throw new IllegalStateException("getServiceComponent() called while not connected" +
-                    " (state=" + mState + ")");
-        }
-        return mServiceComponent;
+        return mImpl.getServiceComponent();
     }
 
     /**
@@ -246,11 +114,7 @@
      * @throws IllegalStateException if not connected.
      */
     public @NonNull String getRoot() {
-        if (!isConnected()) {
-            throw new IllegalStateException("getSessionToken() called while not connected (state="
-                    + getStateLabel(mState) + ")");
-        }
-        return mRootId;
+        return mImpl.getRoot();
     }
 
     /**
@@ -260,11 +124,7 @@
      */
     public @Nullable
     Bundle getExtras() {
-        if (!isConnected()) {
-            throw new IllegalStateException("getExtras() called while not connected (state="
-                    + getStateLabel(mState) + ")");
-        }
-        return mExtras;
+        return mImpl.getExtras();
     }
 
     /**
@@ -279,11 +139,7 @@
      * @throws IllegalStateException if not connected.
      */
      public @NonNull MediaSessionCompat.Token getSessionToken() {
-        if (!isConnected()) {
-            throw new IllegalStateException("getSessionToken() called while not connected (state="
-                    + mState + ")");
-        }
-        return mMediaSessionToken;
+        return mImpl.getSessionToken();
     }
 
     /**
@@ -305,34 +161,7 @@
      * @param callback The callback to receive the list of children.
      */
     public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
-        // Check arguments.
-        if (parentId == null) {
-            throw new IllegalArgumentException("parentId is null");
-        }
-        if (callback == null) {
-            throw new IllegalArgumentException("callback is null");
-        }
-
-        // Update or create the subscription.
-        Subscription sub = mSubscriptions.get(parentId);
-        boolean newSubscription = sub == null;
-        if (newSubscription) {
-            sub = new Subscription(parentId);
-            mSubscriptions.put(parentId, sub);
-        }
-        sub.callback = callback;
-
-        // If we are connected, tell the service that we are watching.  If we aren't
-        // connected, the service will be told when we connect.
-        if (mState == CONNECT_STATE_CONNECTED) {
-            try {
-                mServiceBinder.addSubscription(parentId, mServiceCallbacks);
-            } catch (RemoteException ex) {
-                // Process is crashing.  We will disconnect, and upon reconnect we will
-                // automatically reregister. So nothing to do here.
-                Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
-            }
-        }
+        mImpl.subscribe(parentId, callback);
     }
 
     /**
@@ -346,24 +175,7 @@
      * will be unsubscribed.
      */
     public void unsubscribe(@NonNull String parentId) {
-        // Check arguments.
-        if (TextUtils.isEmpty(parentId)) {
-            throw new IllegalArgumentException("parentId is empty.");
-        }
-
-        // Remove from our list.
-        final Subscription sub = mSubscriptions.remove(parentId);
-
-        // Tell the service if necessary.
-        if (mState == CONNECT_STATE_CONNECTED && sub != null) {
-            try {
-                mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
-            } catch (RemoteException ex) {
-                // Process is crashing.  We will disconnect, and upon reconnect we will
-                // automatically reregister. So nothing to do here.
-                Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
-            }
-        }
+        mImpl.unsubscribe(parentId);
     }
 
     /**
@@ -375,207 +187,7 @@
      * @param cb The callback to receive the result on.
      */
     public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
-        if (TextUtils.isEmpty(mediaId)) {
-            throw new IllegalArgumentException("mediaId is empty.");
-        }
-        if (cb == null) {
-            throw new IllegalArgumentException("cb is null.");
-        }
-        if (mState != CONNECT_STATE_CONNECTED) {
-            Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    cb.onError(mediaId);
-                }
-            });
-            return;
-        }
-        ResultReceiver receiver = new ResultReceiver(mHandler) {
-            @Override
-            protected void onReceiveResult(int resultCode, Bundle resultData) {
-                if (resultCode != 0 || resultData == null
-                        || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
-                    cb.onError(mediaId);
-                    return;
-                }
-                Parcelable item =
-                        resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
-                if (!(item instanceof MediaItem)) {
-                    cb.onError(mediaId);
-                    return;
-                }
-                cb.onItemLoaded((MediaItem)item);
-            }
-        };
-        try {
-            mServiceBinder.getMediaItem(mediaId, receiver);
-        } catch (RemoteException e) {
-            Log.i(TAG, "Remote error getting media item.");
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    cb.onError(mediaId);
-                }
-            });
-        }
-    }
-
-    /**
-     * For debugging.
-     */
-    private static String getStateLabel(int state) {
-        switch (state) {
-            case CONNECT_STATE_DISCONNECTED:
-                return "CONNECT_STATE_DISCONNECTED";
-            case CONNECT_STATE_CONNECTING:
-                return "CONNECT_STATE_CONNECTING";
-            case CONNECT_STATE_CONNECTED:
-                return "CONNECT_STATE_CONNECTED";
-            case CONNECT_STATE_SUSPENDED:
-                return "CONNECT_STATE_SUSPENDED";
-            default:
-                return "UNKNOWN/" + state;
-        }
-    }
-
-    private final void onServiceConnected(final IMediaBrowserServiceCompatCallbacks callback,
-            final String root, final MediaSessionCompat.Token session, final Bundle extra) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                // Check to make sure there hasn't been a disconnect or a different
-                // ServiceConnection.
-                if (!isCurrent(callback, "onConnect")) {
-                    return;
-                }
-                // Don't allow them to call us twice.
-                if (mState != CONNECT_STATE_CONNECTING) {
-                    Log.w(TAG, "onConnect from service while mState="
-                            + getStateLabel(mState) + "... ignoring");
-                    return;
-                }
-                mRootId = root;
-                mMediaSessionToken = session;
-                mExtras = extra;
-                mState = CONNECT_STATE_CONNECTED;
-
-                if (DBG) {
-                    Log.d(TAG, "ServiceCallbacks.onConnect...");
-                    dump();
-                }
-                mCallback.onConnected();
-
-                // we may receive some subscriptions before we are connected, so re-subscribe
-                // everything now
-                for (String id : mSubscriptions.keySet()) {
-                    try {
-                        mServiceBinder.addSubscription(id, mServiceCallbacks);
-                    } catch (RemoteException ex) {
-                        // Process is crashing.  We will disconnect, and upon reconnect we will
-                        // automatically reregister. So nothing to do here.
-                        Log.d(TAG, "addSubscription failed with RemoteException parentId=" + id);
-                    }
-                }
-            }
-        });
-    }
-
-    private final void onConnectionFailed(final IMediaBrowserServiceCompatCallbacks callback) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                Log.e(TAG, "onConnectFailed for " + mServiceComponent);
-
-                // Check to make sure there hasn't been a disconnect or a different
-                // ServiceConnection.
-                if (!isCurrent(callback, "onConnectFailed")) {
-                    return;
-                }
-                // Don't allow them to call us twice.
-                if (mState != CONNECT_STATE_CONNECTING) {
-                    Log.w(TAG, "onConnect from service while mState="
-                            + getStateLabel(mState) + "... ignoring");
-                    return;
-                }
-
-                // Clean up
-                forceCloseConnection();
-
-                // Tell the app.
-                mCallback.onConnectionFailed();
-            }
-        });
-    }
-
-    private final void onLoadChildren(final IMediaBrowserServiceCompatCallbacks callback,
-            final String parentId, final List list) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                // Check that there hasn't been a disconnect or a different
-                // ServiceConnection.
-                if (!isCurrent(callback, "onLoadChildren")) {
-                    return;
-                }
-
-                List<MediaItem> data = list;
-                if (DBG) {
-                    Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
-                }
-                if (data == null) {
-                    data = Collections.emptyList();
-                }
-
-                // Check that the subscription is still subscribed.
-                final Subscription subscription = mSubscriptions.get(parentId);
-                if (subscription == null) {
-                    if (DBG) {
-                        Log.d(TAG, "onLoadChildren for id that isn't subscribed id="
-                                + parentId);
-                    }
-                    return;
-                }
-
-                // Tell the app.
-                subscription.callback.onChildrenLoaded(parentId, data);
-            }
-        });
-    }
-
-    /**
-     * Return true if {@code callback} is the current ServiceCallbacks.  Also logs if it's not.
-     */
-    private boolean isCurrent(IMediaBrowserServiceCompatCallbacks callback, String funcName) {
-        if (mServiceCallbacks != callback) {
-            if (mState != CONNECT_STATE_DISCONNECTED) {
-                Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
-                        + mServiceCallbacks + " this=" + this);
-            }
-            return false;
-        }
-        return true;
-    }
-
-    private ServiceCallbacks getNewServiceCallbacks() {
-        return new ServiceCallbacks(this);
-    }
-
-    /**
-     * Log internal state.
-     * @hide
-     */
-    void dump() {
-        Log.d(TAG, "MediaBrowserCompat...");
-        Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
-        Log.d(TAG, "  mCallback=" + mCallback);
-        Log.d(TAG, "  mRootHints=" + mRootHints);
-        Log.d(TAG, "  mState=" + getStateLabel(mState));
-        Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
-        Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
-        Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
-        Log.d(TAG, "  mRootId=" + mRootId);
-        Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
+        mImpl.getItem(mediaId, cb);
     }
 
     public static class MediaItem implements Parcelable {
@@ -772,140 +384,582 @@
         }
     }
 
-    /**
-     * ServiceConnection to the other app.
-     */
-    private class MediaServiceConnection implements ServiceConnection {
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder binder) {
+    static class MediaBrowserImplBase {
+        private static final String TAG = "MediaBrowserCompat";
+        private static final boolean DBG = false;
+
+        private static final int CONNECT_STATE_DISCONNECTED = 0;
+        private static final int CONNECT_STATE_CONNECTING = 1;
+        private static final int CONNECT_STATE_CONNECTED = 2;
+        private static final int CONNECT_STATE_SUSPENDED = 3;
+
+        private final Context mContext;
+        private final ComponentName mServiceComponent;
+        private final ConnectionCallback mCallback;
+        private final Bundle mRootHints;
+        private final Handler mHandler = new Handler();
+        private final ArrayMap<String,Subscription> mSubscriptions = new ArrayMap<>();
+
+        private int mState = CONNECT_STATE_DISCONNECTED;
+        private MediaServiceConnection mServiceConnection;
+        private IMediaBrowserServiceCompat mServiceBinder;
+        private IMediaBrowserServiceCompatCallbacks mServiceCallbacks;
+        private String mRootId;
+        private MediaSessionCompat.Token mMediaSessionToken;
+        private Bundle mExtras;
+
+        public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
+                ConnectionCallback callback, Bundle rootHints) {
+            if (context == null) {
+                throw new IllegalArgumentException("context must not be null");
+            }
+            if (serviceComponent == null) {
+                throw new IllegalArgumentException("service component must not be null");
+            }
+            if (callback == null) {
+                throw new IllegalArgumentException("connection callback must not be null");
+            }
+            mContext = context;
+            mServiceComponent = serviceComponent;
+            mCallback = callback;
+            mRootHints = rootHints;
+        }
+
+        public void connect() {
+            if (mState != CONNECT_STATE_DISCONNECTED) {
+                throw new IllegalStateException("connect() called while not disconnected (state="
+                        + getStateLabel(mState) + ")");
+            }
+            // TODO: remove this extra check.
             if (DBG) {
-                Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
-                        + " binder=" + binder);
-                dump();
+                if (mServiceConnection != null) {
+                    throw new RuntimeException("mServiceConnection should be null. Instead it is "
+                            + mServiceConnection);
+                }
+            }
+            if (mServiceBinder != null) {
+                throw new RuntimeException("mServiceBinder should be null. Instead it is "
+                        + mServiceBinder);
+            }
+            if (mServiceCallbacks != null) {
+                throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
+                        + mServiceCallbacks);
             }
 
-            // Make sure we are still the current connection, and that they haven't called
-            // disconnect().
-            if (!isCurrent("onServiceConnected")) {
-                return;
-            }
-
-            // Save their binder
-            mServiceBinder = IMediaBrowserServiceCompat.Stub.asInterface(binder);
-
-            // We make a new mServiceCallbacks each time we connect so that we can drop
-            // responses from previous connections.
-            mServiceCallbacks = getNewServiceCallbacks();
             mState = CONNECT_STATE_CONNECTING;
 
-            // Call connect, which is async. When we get a response from that we will
-            // say that we're connected.
+            final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
+            intent.setComponent(mServiceComponent);
+
+            final ServiceConnection thisConnection = mServiceConnection =
+                    new MediaServiceConnection();
+
+            boolean bound = false;
             try {
-                if (DBG) {
-                    Log.d(TAG, "ServiceCallbacks.onConnect...");
-                    dump();
-                }
-                mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
-            } catch (RemoteException ex) {
-                // Connect failed, which isn't good. But the auto-reconnect on the service
-                // will take over and we will come back.  We will also get the
-                // onServiceDisconnected, which has all the cleanup code.  So let that do it.
-                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
-                if (DBG) {
-                    Log.d(TAG, "ServiceCallbacks.onConnect...");
-                    dump();
-                }
+                bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+            } catch (Exception ex) {
+                Log.e(TAG, "Failed binding to service " + mServiceComponent);
+            }
+
+            if (!bound) {
+                // Tell them that it didn't work.  We are already on the main thread,
+                // but we don't want to do callbacks inside of connect().  So post it,
+                // and then check that we are on the same ServiceConnection.  We know
+                // we won't also get an onServiceConnected or onServiceDisconnected,
+                // so we won't be doing double callbacks.
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Ensure that nobody else came in or tried to connect again.
+                        if (thisConnection == mServiceConnection) {
+                            forceCloseConnection();
+                            mCallback.onConnectionFailed();
+                        }
+                    }
+                });
+            }
+
+            if (DBG) {
+                Log.d(TAG, "connect...");
+                dump();
             }
         }
 
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
+        public void disconnect() {
+            // It's ok to call this any state, because allowing this lets apps not have
+            // to check isConnected() unnecessarily.  They won't appreciate the extra
+            // assertions for this.  We do everything we can here to go back to a sane state.
+            if (mServiceCallbacks != null) {
+                try {
+                    mServiceBinder.disconnect(mServiceCallbacks);
+                } catch (RemoteException ex) {
+                    // We are disconnecting anyway.  Log, just for posterity but it's not
+                    // a big problem.
+                    Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+                }
+            }
+            forceCloseConnection();
+
             if (DBG) {
-                Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
-                        + " this=" + this + " mServiceConnection=" + mServiceConnection);
+                Log.d(TAG, "disconnect...");
                 dump();
             }
-
-            // Make sure we are still the current connection, and that they haven't called
-            // disconnect().
-            if (!isCurrent("onServiceDisconnected")) {
-                return;
-            }
-
-            // Clear out what we set in onServiceConnected
-            mServiceBinder = null;
-            mServiceCallbacks = null;
-
-            // And tell the app that it's suspended.
-            mState = CONNECT_STATE_SUSPENDED;
-            mCallback.onConnectionSuspended();
         }
 
         /**
-         * Return true if this is the current ServiceConnection.  Also logs if it's not.
+         * Null out the variables and unbind from the service.  This doesn't include
+         * calling disconnect on the service, because we only try to do that in the
+         * clean shutdown cases.
+         * <p>
+         * Everywhere that calls this EXCEPT for disconnect() should follow it with
+         * a call to mCallback.onConnectionFailed().  Disconnect doesn't do that callback
+         * for a clean shutdown, but everywhere else is a dirty shutdown and should
+         * notify the app.
          */
-        private boolean isCurrent(String funcName) {
-            if (mServiceConnection != this) {
+        private void forceCloseConnection() {
+            if (mServiceConnection != null) {
+                mContext.unbindService(mServiceConnection);
+            }
+            mState = CONNECT_STATE_DISCONNECTED;
+            mServiceConnection = null;
+            mServiceBinder = null;
+            mServiceCallbacks = null;
+            mRootId = null;
+            mMediaSessionToken = null;
+        }
+
+        public boolean isConnected() {
+            return mState == CONNECT_STATE_CONNECTED;
+        }
+
+        public @NonNull
+        ComponentName getServiceComponent() {
+            if (!isConnected()) {
+                throw new IllegalStateException("getServiceComponent() called while not connected" +
+                        " (state=" + mState + ")");
+            }
+            return mServiceComponent;
+        }
+
+        public @NonNull String getRoot() {
+            if (!isConnected()) {
+                throw new IllegalStateException("getSessionToken() called while not connected"
+                        + "(state=" + getStateLabel(mState) + ")");
+            }
+            return mRootId;
+        }
+
+        public @Nullable
+        Bundle getExtras() {
+            if (!isConnected()) {
+                throw new IllegalStateException("getExtras() called while not connected (state="
+                        + getStateLabel(mState) + ")");
+            }
+            return mExtras;
+        }
+
+        public @NonNull MediaSessionCompat.Token getSessionToken() {
+            if (!isConnected()) {
+                throw new IllegalStateException("getSessionToken() called while not connected"
+                        + "(state=" + mState + ")");
+            }
+            return mMediaSessionToken;
+        }
+
+        public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
+            // Check arguments.
+            if (parentId == null) {
+                throw new IllegalArgumentException("parentId is null");
+            }
+            if (callback == null) {
+                throw new IllegalArgumentException("callback is null");
+            }
+
+            // Update or create the subscription.
+            Subscription sub = mSubscriptions.get(parentId);
+            boolean newSubscription = sub == null;
+            if (newSubscription) {
+                sub = new Subscription(parentId);
+                mSubscriptions.put(parentId, sub);
+            }
+            sub.callback = callback;
+
+            // If we are connected, tell the service that we are watching.  If we aren't
+            // connected, the service will be told when we connect.
+            if (mState == CONNECT_STATE_CONNECTED) {
+                try {
+                    mServiceBinder.addSubscription(parentId, mServiceCallbacks);
+                } catch (RemoteException ex) {
+                    // Process is crashing.  We will disconnect, and upon reconnect we will
+                    // automatically reregister. So nothing to do here.
+                    Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
+                }
+            }
+        }
+
+        public void unsubscribe(@NonNull String parentId) {
+            // Check arguments.
+            if (TextUtils.isEmpty(parentId)) {
+                throw new IllegalArgumentException("parentId is empty.");
+            }
+
+            // Remove from our list.
+            final Subscription sub = mSubscriptions.remove(parentId);
+
+            // Tell the service if necessary.
+            if (mState == CONNECT_STATE_CONNECTED && sub != null) {
+                try {
+                    mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
+                } catch (RemoteException ex) {
+                    // Process is crashing.  We will disconnect, and upon reconnect we will
+                    // automatically reregister. So nothing to do here.
+                    Log.d(TAG, "removeSubscription failed with RemoteException parentId="
+                            + parentId);
+                }
+            }
+        }
+
+        public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
+            if (TextUtils.isEmpty(mediaId)) {
+                throw new IllegalArgumentException("mediaId is empty.");
+            }
+            if (cb == null) {
+                throw new IllegalArgumentException("cb is null.");
+            }
+            if (mState != CONNECT_STATE_CONNECTED) {
+                Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        cb.onError(mediaId);
+                    }
+                });
+                return;
+            }
+            ResultReceiver receiver = new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (resultCode != 0 || resultData == null
+                            || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
+                        cb.onError(mediaId);
+                        return;
+                    }
+                    Parcelable item =
+                            resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
+                    if (!(item instanceof MediaItem)) {
+                        cb.onError(mediaId);
+                        return;
+                    }
+                    cb.onItemLoaded((MediaItem)item);
+                }
+            };
+            try {
+                mServiceBinder.getMediaItem(mediaId, receiver);
+            } catch (RemoteException e) {
+                Log.i(TAG, "Remote error getting media item.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        cb.onError(mediaId);
+                    }
+                });
+            }
+        }
+
+        /**
+         * For debugging.
+         */
+        private static String getStateLabel(int state) {
+            switch (state) {
+                case CONNECT_STATE_DISCONNECTED:
+                    return "CONNECT_STATE_DISCONNECTED";
+                case CONNECT_STATE_CONNECTING:
+                    return "CONNECT_STATE_CONNECTING";
+                case CONNECT_STATE_CONNECTED:
+                    return "CONNECT_STATE_CONNECTED";
+                case CONNECT_STATE_SUSPENDED:
+                    return "CONNECT_STATE_SUSPENDED";
+                default:
+                    return "UNKNOWN/" + state;
+            }
+        }
+
+        private final void onServiceConnected(final IMediaBrowserServiceCompatCallbacks callback,
+                final String root, final MediaSessionCompat.Token session, final Bundle extra) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    // Check to make sure there hasn't been a disconnect or a different
+                    // ServiceConnection.
+                    if (!isCurrent(callback, "onConnect")) {
+                        return;
+                    }
+                    // Don't allow them to call us twice.
+                    if (mState != CONNECT_STATE_CONNECTING) {
+                        Log.w(TAG, "onConnect from service while mState="
+                                + getStateLabel(mState) + "... ignoring");
+                        return;
+                    }
+                    mRootId = root;
+                    mMediaSessionToken = session;
+                    mExtras = extra;
+                    mState = CONNECT_STATE_CONNECTED;
+
+                    if (DBG) {
+                        Log.d(TAG, "ServiceCallbacks.onConnect...");
+                        dump();
+                    }
+                    mCallback.onConnected();
+
+                    // we may receive some subscriptions before we are connected, so re-subscribe
+                    // everything now
+                    for (String id : mSubscriptions.keySet()) {
+                        try {
+                            mServiceBinder.addSubscription(id, mServiceCallbacks);
+                        } catch (RemoteException ex) {
+                            // Process is crashing.  We will disconnect, and upon reconnect we will
+                            // automatically reregister. So nothing to do here.
+                            Log.d(TAG, "addSubscription failed with RemoteException parentId="
+                                    + id);
+                        }
+                    }
+                }
+            });
+        }
+
+        private final void onConnectionFailed(final IMediaBrowserServiceCompatCallbacks callback) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+                    // Check to make sure there hasn't been a disconnect or a different
+                    // ServiceConnection.
+                    if (!isCurrent(callback, "onConnectFailed")) {
+                        return;
+                    }
+                    // Don't allow them to call us twice.
+                    if (mState != CONNECT_STATE_CONNECTING) {
+                        Log.w(TAG, "onConnect from service while mState="
+                                + getStateLabel(mState) + "... ignoring");
+                        return;
+                    }
+
+                    // Clean up
+                    forceCloseConnection();
+
+                    // Tell the app.
+                    mCallback.onConnectionFailed();
+                }
+            });
+        }
+
+        private final void onLoadChildren(final IMediaBrowserServiceCompatCallbacks callback,
+                final String parentId, final List list) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    // Check that there hasn't been a disconnect or a different
+                    // ServiceConnection.
+                    if (!isCurrent(callback, "onLoadChildren")) {
+                        return;
+                    }
+
+                    List<MediaItem> data = list;
+                    if (DBG) {
+                        Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
+                    }
+                    if (data == null) {
+                        data = Collections.emptyList();
+                    }
+
+                    // Check that the subscription is still subscribed.
+                    final Subscription subscription = mSubscriptions.get(parentId);
+                    if (subscription == null) {
+                        if (DBG) {
+                            Log.d(TAG, "onLoadChildren for id that isn't subscribed id="
+                                    + parentId);
+                        }
+                        return;
+                    }
+
+                    // Tell the app.
+                    subscription.callback.onChildrenLoaded(parentId, data);
+                }
+            });
+        }
+
+        /**
+         * Return true if {@code callback} is the current ServiceCallbacks.  Also logs if it's not.
+         */
+        private boolean isCurrent(IMediaBrowserServiceCompatCallbacks callback, String funcName) {
+            if (mServiceCallbacks != callback) {
                 if (mState != CONNECT_STATE_DISCONNECTED) {
-                    // Check mState, because otherwise this log is noisy.
                     Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
-                            + mServiceConnection + " this=" + this);
+                            + mServiceCallbacks + " this=" + this);
                 }
                 return false;
             }
             return true;
         }
-    }
 
-    /**
-     * Callbacks from the service.
-     */
-    private static class ServiceCallbacks extends IMediaBrowserServiceCompatCallbacks.Stub {
-        private WeakReference<MediaBrowserCompat> mMediaBrowser;
-
-        public ServiceCallbacks(MediaBrowserCompat mediaBrowser) {
-            mMediaBrowser = new WeakReference<MediaBrowserCompat>(mediaBrowser);
+        private ServiceCallbacks getNewServiceCallbacks() {
+            return new ServiceCallbacks(this);
         }
 
         /**
-         * The other side has acknowledged our connection.  The parameters to this function
-         * are the initial data as requested.
+         * Log internal state.
+         * @hide
          */
-        @Override
-        public void onConnect(final String root, final MediaSessionCompat.Token session,
-                final Bundle extras) {
-            MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
-            if (mediaBrowser != null) {
-                mediaBrowser.onServiceConnected(this, root, session, extras);
+        void dump() {
+            Log.d(TAG, "MediaBrowserCompat...");
+            Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
+            Log.d(TAG, "  mCallback=" + mCallback);
+            Log.d(TAG, "  mRootHints=" + mRootHints);
+            Log.d(TAG, "  mState=" + getStateLabel(mState));
+            Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
+            Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
+            Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
+            Log.d(TAG, "  mRootId=" + mRootId);
+            Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
+        }
+
+        /**
+         * ServiceConnection to the other app.
+         */
+        private class MediaServiceConnection implements ServiceConnection {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder binder) {
+                if (DBG) {
+                    Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+                            + " binder=" + binder);
+                    dump();
+                }
+
+                // Make sure we are still the current connection, and that they haven't called
+                // disconnect().
+                if (!isCurrent("onServiceConnected")) {
+                    return;
+                }
+
+                // Save their binder
+                mServiceBinder = IMediaBrowserServiceCompat.Stub.asInterface(binder);
+
+                // We make a new mServiceCallbacks each time we connect so that we can drop
+                // responses from previous connections.
+                mServiceCallbacks = getNewServiceCallbacks();
+                mState = CONNECT_STATE_CONNECTING;
+
+                // Call connect, which is async. When we get a response from that we will
+                // say that we're connected.
+                try {
+                    if (DBG) {
+                        Log.d(TAG, "ServiceCallbacks.onConnect...");
+                        dump();
+                    }
+                    mServiceBinder.connect(
+                            mContext.getPackageName(), mRootHints, mServiceCallbacks);
+                } catch (RemoteException ex) {
+                    // Connect failed, which isn't good. But the auto-reconnect on the service
+                    // will take over and we will come back.  We will also get the
+                    // onServiceDisconnected, which has all the cleanup code.  So let that do it.
+                    Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+                    if (DBG) {
+                        Log.d(TAG, "ServiceCallbacks.onConnect...");
+                        dump();
+                    }
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                if (DBG) {
+                    Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+                            + " this=" + this + " mServiceConnection=" + mServiceConnection);
+                    dump();
+                }
+
+                // Make sure we are still the current connection, and that they haven't called
+                // disconnect().
+                if (!isCurrent("onServiceDisconnected")) {
+                    return;
+                }
+
+                // Clear out what we set in onServiceConnected
+                mServiceBinder = null;
+                mServiceCallbacks = null;
+
+                // And tell the app that it's suspended.
+                mState = CONNECT_STATE_SUSPENDED;
+                mCallback.onConnectionSuspended();
+            }
+
+            /**
+             * Return true if this is the current ServiceConnection.  Also logs if it's not.
+             */
+            private boolean isCurrent(String funcName) {
+                if (mServiceConnection != this) {
+                    if (mState != CONNECT_STATE_DISCONNECTED) {
+                        // Check mState, because otherwise this log is noisy.
+                        Log.i(TAG, funcName + " for " + mServiceComponent +
+                                " with mServiceConnection=" + mServiceConnection + " this=" + this);
+                    }
+                    return false;
+                }
+                return true;
             }
         }
 
         /**
-         * The other side does not like us.  Tell the app via onConnectionFailed.
+         * Callbacks from the service.
          */
-        @Override
-        public void onConnectFailed() {
-            MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
-            if (mediaBrowser != null) {
-                mediaBrowser.onConnectionFailed(this);
+        private static class ServiceCallbacks extends IMediaBrowserServiceCompatCallbacks.Stub {
+            private WeakReference<MediaBrowserImplBase> mMediaBrowser;
+
+            public ServiceCallbacks(MediaBrowserImplBase mediaBrowser) {
+                mMediaBrowser = new WeakReference<>(mediaBrowser);
+            }
+
+            /**
+             * The other side has acknowledged our connection.  The parameters to this function
+             * are the initial data as requested.
+             */
+            @Override
+            public void onConnect(final String root, final MediaSessionCompat.Token session,
+                    final Bundle extras) {
+                MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+                if (mediaBrowser != null) {
+                    mediaBrowser.onServiceConnected(this, root, session, extras);
+                }
+            }
+
+            /**
+             * The other side does not like us.  Tell the app via onConnectionFailed.
+             */
+            @Override
+            public void onConnectFailed() {
+                MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+                if (mediaBrowser != null) {
+                    mediaBrowser.onConnectionFailed(this);
+                }
+            }
+
+            @Override
+            public void onLoadChildren(final String parentId, final List list) {
+                MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+                if (mediaBrowser != null) {
+                    mediaBrowser.onLoadChildren(this, parentId, list);
+                }
             }
         }
 
-        @Override
-        public void onLoadChildren(final String parentId, final List list) {
-            MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
-            if (mediaBrowser != null) {
-                mediaBrowser.onLoadChildren(this, parentId, list);
+        private static class Subscription {
+            final String id;
+            SubscriptionCallback callback;
+
+            Subscription(String id) {
+                this.id = id;
             }
         }
     }
-
-    private static class Subscription {
-        final String id;
-        SubscriptionCallback callback;
-
-        Subscription(String id) {
-            this.id = id;
-        }
-    }
 }
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index 0f680a5..48f4750 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -117,7 +117,7 @@
             }
 
             pom.project {
-                name 'Android Support Library v4'
+                name 'Android MediaRouter Support Library'
                 description "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 4 or later."
                 url 'http://developer.android.com/tools/extras/support-library.html'
                 inceptionYear '2011'
diff --git a/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml b/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml
new file mode 100644
index 0000000..a51bfce
--- /dev/null
+++ b/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.4"
+    android:controlY1="0"
+    android:controlX2="0.2"
+    android:controlY2="1"/>
diff --git a/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml b/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml
new file mode 100644
index 0000000..d500c8b
--- /dev/null
+++ b/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0"
+    android:controlY1="0"
+    android:controlX2="0.2"
+    android:controlY2="1"/>
diff --git a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
index 8d17c25..b7cc517 100644
--- a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
+++ b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
@@ -14,41 +14,46 @@
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="fill_parent"
-              android:layout_height="wrap_content"
-              android:orientation="vertical">
-    <LinearLayout android:id="@+id/mr_title_bar"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/mr_expandable_area"
+          android:background="@android:color/transparent"
+          android:layout_width="fill_parent"
+          android:layout_height="fill_parent">
+    <LinearLayout android:id="@+id/mr_dialog_area"
                   android:layout_width="fill_parent"
                   android:layout_height="wrap_content"
-                  android:paddingLeft="24dp"
-                  android:paddingRight="12dp"
-                  android:orientation="horizontal" >
-        <TextView android:id="@+id/mr_name"
-                  android:layout_width="0dp"
-                  android:layout_height="72dp"
-                  android:layout_weight="1"
-                  android:gravity="center_vertical"
-                  android:singleLine="true"
-                  android:ellipsize="end"
-                  android:textAppearance="?attr/mediaRouteControllerTitleTextStyle" />
-        <ImageButton android:id="@+id/mr_close"
-                     android:layout_width="48dp"
-                     android:layout_height="48dp"
-                     android:layout_gravity="center_vertical"
-                     android:contentDescription="@string/mr_controller_close_description"
-                     android:src="?attr/mediaRouteCloseDrawable"
-                     android:background="?attr/selectableItemBackgroundBorderless" />
-    </LinearLayout>
-    <FrameLayout android:id="@+id/mr_custom_control"
-                 android:layout_width="fill_parent"
-                 android:layout_height="wrap_content"
-                 android:visibility="gone" />
-    <FrameLayout android:id="@+id/mr_default_control"
-                 android:layout_width="fill_parent"
-                 android:layout_height="wrap_content">
-        <FrameLayout android:layout_width="fill_parent"
-                     android:layout_height="fill_parent">
+                  android:background="?android:attr/windowBackground"
+                  android:layout_gravity="center"
+                  android:orientation="vertical">
+        <LinearLayout android:id="@+id/mr_title_bar"
+                      android:layout_width="fill_parent"
+                      android:layout_height="wrap_content"
+                      android:paddingLeft="24dp"
+                      android:paddingRight="12dp"
+                      android:orientation="horizontal" >
+            <TextView android:id="@+id/mr_name"
+                      android:layout_width="0dp"
+                      android:layout_height="72dp"
+                      android:layout_weight="1"
+                      android:gravity="center_vertical"
+                      android:singleLine="true"
+                      android:ellipsize="end"
+                      android:textAppearance="?attr/mediaRouteControllerTitleTextStyle" />
+            <ImageButton android:id="@+id/mr_close"
+                         android:layout_width="48dp"
+                         android:layout_height="48dp"
+                         android:layout_gravity="center_vertical"
+                         android:contentDescription="@string/mr_controller_close_description"
+                         android:src="?attr/mediaRouteCloseDrawable"
+                         android:background="?attr/selectableItemBackgroundBorderless" />
+        </LinearLayout>
+        <FrameLayout android:id="@+id/mr_custom_control"
+                     android:layout_width="fill_parent"
+                     android:layout_height="wrap_content"
+                     android:visibility="gone" />
+        <FrameLayout android:id="@+id/mr_default_control"
+                     android:layout_width="fill_parent"
+                     android:layout_height="wrap_content">
             <ImageView android:id="@+id/mr_art"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content"
@@ -57,35 +62,39 @@
                        android:background="?attr/colorPrimary"
                        android:layout_gravity="top"
                        android:visibility="gone" />
-            <ListView android:id="@+id/mr_volume_group_list"
-                      android:layout_width="fill_parent"
-                      android:layout_height="wrap_content"
-                      android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
-                      android:scrollbarStyle="outsideInset"
-                      android:clipToPadding="false"
-                      android:background="?attr/colorPrimaryDark"
-                      android:layout_gravity="bottom"
-                      android:visibility="gone" />
+            <LinearLayout android:layout_width="fill_parent"
+                          android:layout_height="wrap_content"
+                          android:orientation="vertical"
+                          android:layout_gravity="bottom">
+                <LinearLayout android:id="@+id/mr_media_main_control"
+                              android:layout_width="fill_parent"
+                              android:layout_height="wrap_content"
+                              android:orientation="vertical"
+                              android:paddingTop="16dp"
+                              android:paddingBottom="16dp"
+                              android:background="?attr/colorPrimary"
+                              android:layout_gravity="bottom">
+                    <include android:id="@+id/mr_playback_control"
+                             layout="@layout/mr_playback_control" />
+                    <View android:id="@+id/mr_control_divider"
+                          android:layout_width="fill_parent"
+                          android:layout_height="8dp"
+                          android:background="?attr/colorPrimary"
+                          android:visibility="gone" />
+                    <include android:id="@+id/mr_volume_control"
+                             layout="@layout/mr_volume_control" />
+                </LinearLayout>
+                <ListView android:id="@+id/mr_volume_group_list"
+                          android:layout_width="fill_parent"
+                          android:layout_height="wrap_content"
+                          android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
+                          android:scrollbarStyle="outsideInset"
+                          android:clipToPadding="false"
+                          android:background="?attr/colorPrimaryDark"
+                          android:transcriptMode="alwaysScroll"
+                          android:visibility="gone" />
+            </LinearLayout>
         </FrameLayout>
-        <LinearLayout android:id="@+id/mr_media_main_control"
-                      android:layout_width="fill_parent"
-                      android:layout_height="wrap_content"
-                      android:orientation="vertical"
-                      android:paddingTop="16dp"
-                      android:paddingBottom="16dp"
-                      android:background="?attr/colorPrimary"
-                      android:layout_gravity="bottom">
-            <include android:id="@+id/mr_playback_control"
-                     layout="@layout/mr_playback_control" />
-            <View android:id="@+id/mr_control_divider"
-                  android:layout_width="fill_parent"
-                  android:layout_height="8dp"
-                  android:background="?attr/colorPrimary"
-                  android:visibility="gone" />
-            <include android:id="@+id/mr_volume_control"
-                     layout="@layout/mr_volume_control" />
-        </LinearLayout>
-    </FrameLayout>
-
-    <include layout="@layout/abc_alert_dialog_button_bar_material" />
-</LinearLayout>
+        <include layout="@layout/abc_alert_dialog_button_bar_material" />
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/v7/mediarouter/res/layout/mr_controller_volume_item.xml b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
index 6cd0fd6..3b6ce71 100644
--- a/v7/mediarouter/res/layout/mr_controller_volume_item.xml
+++ b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
@@ -39,6 +39,8 @@
                    android:src="?attr/mediaRouteAudioTrackDrawable" />
         <android.support.v7.app.MediaRouteVolumeSlider android:id="@+id/mr_volume_slider"
                  android:layout_width="fill_parent"
-                 android:layout_height="wrap_content" />
+                 android:layout_height="wrap_content"
+                 android:minHeight="40dp"
+                 android:maxHeight="40dp" />
     </LinearLayout>
 </LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_volume_control.xml b/v7/mediarouter/res/layout/mr_volume_control.xml
index 18d7864..d231c7a 100644
--- a/v7/mediarouter/res/layout/mr_volume_control.xml
+++ b/v7/mediarouter/res/layout/mr_volume_control.xml
@@ -32,6 +32,8 @@
             android:id="@+id/mr_volume_slider"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:minHeight="48dp"
+            android:maxHeight="48dp"
             android:layout_weight="1"/>
     <android.support.v7.app.MediaRouteExpandCollapseButton
             android:id="@+id/mr_group_expand_collapse"
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
index f4797cf..2b98398 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -107,8 +107,10 @@
     private ImageButton mCloseButton;
     private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;
 
-    private FrameLayout mCustomControlLayout;
+    private FrameLayout mExpandableAreaLayout;
+    private LinearLayout mDialogAreaLayout;
     private FrameLayout mDefaultControlLayout;
+    private FrameLayout mCustomControlLayout;
     private ImageView mArtView;
     private TextView mTitleView;
     private TextView mSubtitleView;
@@ -268,6 +270,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        getWindow().setBackgroundDrawableResource(android.R.color.transparent);
         setContentView(R.layout.mr_controller_material_dialog_b);
 
         // Remove the neutral button.
@@ -275,6 +278,20 @@
 
         ClickListener listener = new ClickListener();
 
+        mExpandableAreaLayout = (FrameLayout) findViewById(R.id.mr_expandable_area);
+        mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                dismiss();
+            }
+        });
+        mDialogAreaLayout = (LinearLayout) findViewById(R.id.mr_dialog_area);
+        mDialogAreaLayout.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Eat unhandled touch events.
+            }
+        });
         int color = MediaRouterThemeHelper.getButtonTextColor(mContext);
         mDisconnectButton = (Button) findViewById(BUTTON_DISCONNECT_RES_ID);
         mDisconnectButton.setText(R.string.mr_controller_disconnect);
@@ -328,7 +345,7 @@
             }
         });
         mGroupListAnimationDurationMs = mContext.getResources().getInteger(
-                        R.integer.mr_controller_volume_group_list_animation_duration_ms);
+                R.integer.mr_controller_volume_group_list_animation_duration_ms);
 
         mCustomControlView = onCreateMediaControlView(savedInstanceState);
         if (mCustomControlView != null) {
@@ -481,14 +498,14 @@
             return;
         }
         // Measure the size of widgets and get the height of main components.
+        int oldHeight = getLayoutHeight(mMediaMainControlLayout);
+        setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.FILL_PARENT);
         updateMediaControlVisibility(isPlaybackControlAvailable());
-        int oldBottomMargin = getLayoutBottomMargin(mMediaMainControlLayout);
-        setLayoutBottomMargin(mMediaMainControlLayout, 0);
         View decorView = getWindow().getDecorView();
         decorView.measure(
                 MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY),
                 MeasureSpec.UNSPECIFIED);
-        setLayoutBottomMargin(mMediaMainControlLayout, oldBottomMargin);
+        setLayoutHeight(mMediaMainControlLayout, oldHeight);
         int artViewHeight = 0;
         if (mArtView.getDrawable() instanceof BitmapDrawable) {
             Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap();
@@ -502,16 +519,8 @@
         int volumeGroupListCount = mVolumeGroupList.getAdapter() != null
                 ? mVolumeGroupList.getAdapter().getCount() : 0;
         // Scale down volume group list items in landscape mode.
-        for (int i = 0; i < volumeGroupListCount; i++) {
-            View item = mVolumeGroupList.getChildAt(i);
-            if (item != null) {
-                setLayoutHeight(item, mVolumeGroupListItemHeight);
-                View icon = item.findViewById(R.id.mr_volume_item_icon);
-                ViewGroup.LayoutParams lp = icon.getLayoutParams();
-                lp.width = mVolumeGroupListItemIconSize;
-                lp.height = mVolumeGroupListItemIconSize;
-                icon.setLayoutParams(lp);
-            }
+        for (int i = 0; i < mVolumeGroupList.getChildCount(); i++) {
+            updateVolumeGroupItemHeight(mVolumeGroupList.getChildAt(i));
         }
         int expandedGroupListHeight = mVolumeGroupListItemHeight * volumeGroupListCount;
         if (volumeGroupListCount > 0) {
@@ -527,7 +536,7 @@
         // Height of non-control views in decor view.
         // This includes title bar, button bar, and dialog's vertical padding which should be
         // always shown.
-        int nonControlViewHeight = decorView.getMeasuredHeight()
+        int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight()
                 - mDefaultControlLayout.getMeasuredHeight();
         // Maximum allowed height for controls to fit screen.
         int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight;
@@ -537,10 +546,14 @@
             mArtView.setVisibility(View.VISIBLE);
             setLayoutHeight(mArtView, artViewHeight);
         } else {
+            if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight()
+                    >= mDefaultControlLayout.getMeasuredHeight()) {
+                mArtView.setVisibility(View.GONE);
+            }
             artViewHeight = 0;
             desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight;
         }
-        // Show control if it fits the screen
+        // Show the playback control if it fits the screen.
         if (isPlaybackControlAvailable()
                 && desiredControlLayoutHeight <= maximumControlViewHeight) {
             mPlaybackControl.setVisibility(View.VISIBLE);
@@ -558,82 +571,49 @@
             visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight);
             desiredControlLayoutHeight = maximumControlViewHeight;
         }
-        setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
-
-        // Animate the main control position if needed.
-        if (mVolumeGroupList.getVisibility() == View.VISIBLE
-                && mArtView.getVisibility() == View.VISIBLE && mIsGroupListAnimationNeeded) {
-            setLayoutHeight(mVolumeGroupList, mIsGroupExpanded ? expandedGroupListHeight
-                    : Math.min(mArtView.getHeight(), getLayoutHeight(mVolumeGroupList)));
-            updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight,
-                    true /* animation */);
+        // Update the layouts with the computed heights.
+        mMediaMainControlLayout.clearAnimation();
+        mVolumeGroupList.clearAnimation();
+        mDefaultControlLayout.clearAnimation();
+        if (mIsGroupListAnimationNeeded) {
+            animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+            animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+            animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
         } else {
-            // Rely on AlertDialog's animation if there is no art work.
-            // TODO: Add group list animation even when there is no art work.
+            setLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
             setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
-            updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight,
-                    false /* animation */);
-            if (artViewHeight == 0) {
-                mArtView.setVisibility(View.GONE);
-            }
-            if (!mIsGroupExpanded) {
-                mVolumeGroupList.setVisibility(View.GONE);
-            }
+            setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
         }
         mIsGroupListAnimationNeeded = false;
+        // Maximize the window size with a transparent layout in advance for smooth animation.
+        setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
     }
 
-    private void updateMainControlBottomMargin(final int bottomMargin,
-            final int mainControllerHeight, boolean animation) {
-        final boolean isExpanding = bottomMargin != 0;
-        if (!animation) {
-            setLayoutBottomMargin(mMediaMainControlLayout, bottomMargin);
-            View frontView = isExpanding ? mVolumeGroupList : mArtView;
-            frontView.bringToFront();
-            ((View) frontView.getParent()).invalidate();
-        } else {
-            Animation existingAnim = mMediaMainControlLayout.getAnimation();
-            boolean animationInProgress = existingAnim != null && !existingAnim.hasEnded();
-            if (animationInProgress) {
-                mMediaMainControlLayout.clearAnimation();
-            }
-            final int volumeGroupListHeight = getLayoutHeight(mVolumeGroupList);
-            int rightBelowArtWork = getLayoutHeight(mDefaultControlLayout)
-                    - mArtView.getHeight() - mainControllerHeight;
-            final int startValue = animationInProgress
-                    ? getLayoutBottomMargin(mMediaMainControlLayout)
-                    : isExpanding ? rightBelowArtWork : volumeGroupListHeight;
-            final int endValue = bottomMargin;
-            Animation anim = new Animation() {
-                private boolean mReordered;
+    private void updateVolumeGroupItemHeight(View item) {
+        setLayoutHeight(item, mVolumeGroupListItemHeight);
+        View icon = item.findViewById(R.id.mr_volume_item_icon);
+        ViewGroup.LayoutParams lp = icon.getLayoutParams();
+        lp.width = mVolumeGroupListItemIconSize;
+        lp.height = mVolumeGroupListItemIconSize;
+        icon.setLayoutParams(lp);
+    }
 
-                @Override
-                protected void applyTransformation(float interpolatedTime, Transformation t) {
-                    int margin = startValue - (int) ((startValue - endValue) * interpolatedTime);
-                    setLayoutBottomMargin(mMediaMainControlLayout, margin);
-                    // Since there could be an overlapping area of the artwork and volume group list
-                    // , z-order of the art work and volume group list should be exchanged when the
-                    // main control covers the overlapping area.
-                    if (!mReordered) {
-                        if (isExpanding) {
-                            if (margin + mainControllerHeight >= volumeGroupListHeight) {
-                                mVolumeGroupList.bringToFront();
-                                ((View) mVolumeGroupList.getParent()).invalidate();
-                                mReordered = true;
-                            }
-                        } else {
-                            if (volumeGroupListHeight >= margin + mainControllerHeight) {
-                                mArtView.bringToFront();
-                                ((View) mArtView.getParent()).invalidate();
-                                mReordered = true;
-                            }
-                        }
-                    }
-                }
-            };
-            anim.setDuration(mGroupListAnimationDurationMs);
-            mMediaMainControlLayout.startAnimation(anim);
+    private void animateLayoutHeight(final View view, int targetHeight) {
+        final int startValue = getLayoutHeight(view);
+        final int endValue = targetHeight;
+        Animation anim = new Animation() {
+            @Override
+            protected void applyTransformation(float interpolatedTime, Transformation t) {
+                int height = startValue - (int) ((startValue - endValue) * interpolatedTime);
+                setLayoutHeight(view, height);
+            }
+        };
+        anim.setDuration(mGroupListAnimationDurationMs);
+        if (android.os.Build.VERSION.SDK_INT >= 21) {
+            anim.setInterpolator(mContext, mIsGroupExpanded ? R.interpolator.mr_linear_out_slow_in
+                    : R.interpolator.mr_fast_out_slow_in);
         }
+        view.startAnimation(anim);
     }
 
     private void updateVolumeControl() {
@@ -649,7 +629,7 @@
                     VolumeGroupAdapter adapter =
                             (VolumeGroupAdapter) mVolumeGroupList.getAdapter();
                     if (adapter != null) {
-                        adapter.notifyDataSetChanged();
+                       adapter.notifyDataSetChanged();
                     }
                 }
             } else {
@@ -902,6 +882,8 @@
             if (v == null) {
                 v = LayoutInflater.from(mContext).inflate(
                         R.layout.mr_controller_volume_item, parent, false);
+            } else {
+                updateVolumeGroupItemHeight(v);
             }
 
             MediaRouter.RouteInfo route = getItem(position);
diff --git a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
index 66ebab5..fd1cee4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
@@ -339,6 +339,25 @@
         }
     }
 
+    /**
+     * Moves a child view from hidden list to regular list.
+     * Calling this method should probably be followed by a detach, otherwise, it will suddenly
+     * show up in LayoutManager's children list.
+     *
+     * @param view The hidden View to unhide
+     */
+    void unhide(View view) {
+        final int offset = mCallback.indexOfChild(view);
+        if (offset < 0) {
+            throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+        }
+        if (!mBucket.get(offset)) {
+            throw new RuntimeException("trying to unhide a view that was not hidden" + view);
+        }
+        mBucket.clear(offset);
+        unhideViewInternal(view);
+    }
+
     @Override
     public String toString() {
         return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index ee4278f..f0b0afe 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -72,6 +72,7 @@
 import java.util.List;
 
 import static android.support.v7.widget.AdapterHelper.Callback;
+import static android.support.v7.widget.AdapterHelper.POSITION_TYPE_INVISIBLE;
 import static android.support.v7.widget.AdapterHelper.UpdateOp;
 import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
 
@@ -2773,8 +2774,8 @@
         onEnterLayoutOrScroll();
 
         processAdapterUpdatesAndSetAnimationFlags();
-        final boolean trackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
-        if (trackOldChangeHolders && mState.mOldChangedHolders == null) {
+        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
+        if (mState.mTrackOldChangeHolders && mState.mOldChangedHolders == null) {
             mState.mOldChangedHolders = new ArrayMap<>();
         }
         mItemsAddedOrRemoved = mItemsChanged = false;
@@ -2798,9 +2799,16 @@
                                 ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                 holder.getUnmodifiedPayloads());
                 mState.mPreLayoutHolderMap.put(holder, animationInfo);
-                if (trackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
+                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                         && !holder.shouldIgnore() && !holder.isInvalid()) {
                     long key = getChangedHolderKey(holder);
+                    // This is NOT the only place where a ViewHolder is added to old change holders
+                    // list. There is another case where:
+                    //    * A VH is currently hidden but not deleted
+                    //    * The hidden item is changed in the adapter
+                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
+                    // When this case is detected, RV will un-hide that view and add to the old
+                    // change holders list.
                     mState.mOldChangedHolders.put(key, holder);
                 }
             }
@@ -2836,10 +2844,18 @@
                 }
                 if (!found) {
                     int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
-                    flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    boolean wasHidden = viewHolder
+                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    if (!wasHidden) {
+                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    }
                     final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                             mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
-                    appearingViewInfo.put(child, animationInfo);
+                    if (wasHidden) {
+                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
+                    } else {
+                        appearingViewInfo.put(child, animationInfo);
+                    }
                 }
             }
             // we don't process disappearing list because they may re-appear in post layout pass.
@@ -2872,7 +2888,7 @@
                 long key = getChangedHolderKey(holder);
                 final ItemHolderInfo animationInfo = mItemAnimator
                         .recordPostLayoutInformation(mState, holder);
-                ViewHolder oldChangeViewHolder = trackOldChangeHolders ?
+                ViewHolder oldChangeViewHolder = mState.mTrackOldChangeHolders ?
                         mState.mOldChangedHolders.get(key) : null;
                 if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                     // run a change animation
@@ -2938,6 +2954,8 @@
         mState.mPreviousLayoutItemCount = mState.mItemCount;
         mDataSetHasChangedAfterLayout = false;
         mState.mRunSimpleAnimations = false;
+        mState.mPreLayoutHolderMap.clear();
+        mState.mPostLayoutHolderMap.clear();
         mState.mRunPredictiveAnimations = false;
         onExitLayoutOrScroll();
         mLayout.mRequestedSimpleAnimations = false;
@@ -2953,6 +2971,22 @@
         }
     }
 
+    /**
+     * Records the animation information for a view holder that was bounced from hidden list. It
+     * also clears the bounce back flag.
+     */
+    private void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
+            ItemHolderInfo animationInfo) {
+        // looks like this view bounced back from hidden list!
+        viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+        if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
+                && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
+            long key = getChangedHolderKey(viewHolder);
+            mState.mOldChangedHolders.put(key, viewHolder);
+        }
+        mState.mPreLayoutHolderMap.put(viewHolder, animationInfo);
+    }
+
     private void findMinMaxChildLayoutPositions(int[] into) {
         final int count = mChildHelper.getChildCount();
         if (count == 0) {
@@ -4466,6 +4500,23 @@
                     }
                 }
             }
+
+            // This is very ugly but the only place we can grab this information
+            // before the View is rebound and returned to the LayoutManager for post layout ops.
+            // We don't need this in pre-layout since the VH is not updated by the LM.
+            if (fromScrap && !mState.isPreLayout() && holder
+                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
+                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                if (mState.mRunSimpleAnimations) {
+                    int changeFlags = ItemAnimator
+                            .buildAdapterChangeFlagsForAnimations(holder);
+                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
+                            holder, changeFlags, holder.getUnmodifiedPayloads());
+                    recordAnimationInfoIfBouncedHiddenView(holder, info);
+                }
+            }
+
             boolean bound = false;
             if (mState.isPreLayout() && holder.isBound()) {
                 // do not update unless we absolutely have to.
@@ -4812,8 +4863,20 @@
             if (!dryRun) {
                 View view = mChildHelper.findHiddenNonRemovedView(position, type);
                 if (view != null) {
-                    // ending the animation should cause it to get recycled before we reuse it
-                    mItemAnimator.endAnimation(getChildViewHolder(view));
+                    // This View is good to be used. We just need to unhide, detach and move to the
+                    // scrap list.
+                    final ViewHolder vh = getChildViewHolderInt(view);
+                    mChildHelper.unhide(view);
+                    int layoutIndex = mChildHelper.indexOfChild(view);
+                    if (layoutIndex == RecyclerView.NO_POSITION) {
+                        throw new IllegalStateException("layout index should not be -1 after "
+                                + "unhiding a view:" + vh);
+                    }
+                    mChildHelper.detachViewFromParent(layoutIndex);
+                    scrapView(view);
+                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
+                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    return vh;
                 }
             }
 
@@ -8192,6 +8255,21 @@
          */
         static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
 
+        /**
+         * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
+         * hidden list (as if it was scrap) without being recycled in between.
+         *
+         * When a ViewHolder is hidden, there are 2 paths it can be re-used:
+         *   a) Animation ends, view is recycled and used from the recycle pool.
+         *   b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
+         *
+         * This flag is used to represent "case b" where the ViewHolder is reused without being
+         * recycled (thus "bounced" from the hidden list). This state requires special handling
+         * because the ViewHolder must be added to pre layout maps for animations as if it was
+         * already there.
+         */
+        static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+
         private int mFlags;
 
         private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
@@ -9344,6 +9422,8 @@
 
         private boolean mRunPredictiveAnimations = false;
 
+        private boolean mTrackOldChangeHolders = false;
+
         State reset() {
             mTargetPosition = RecyclerView.NO_POSITION;
             if (mData != null) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
index 1d095b4..e690f22 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
@@ -630,4 +630,69 @@
             }
         }
     }
+
+    static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
+        final RecyclerView.ViewHolder viewHolder;
+        @RecyclerView.ItemAnimator.AdapterChanges
+        final int changeFlags;
+        final List<Object> payloads;
+
+        LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
+            this.viewHolder = viewHolder;
+            this.changeFlags = changeFlags;
+            if (payloads != null) {
+                this.payloads = new ArrayList<>();
+                this.payloads.addAll(payloads);
+            } else {
+                this.payloads = null;
+            }
+            setFrom(viewHolder);
+        }
+    }
+
+    static class AnimateChange extends AnimatePersistence {
+
+        final RecyclerView.ViewHolder newHolder;
+
+        public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
+                LoggingInfo pre, LoggingInfo post) {
+            super(oldHolder, pre, post);
+            this.newHolder = newHolder;
+        }
+    }
+
+    static class AnimatePersistence extends AnimateAppearance {
+
+        public AnimatePersistence(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+                LoggingInfo post) {
+            super(viewHolder, pre, post);
+        }
+    }
+
+    static class AnimateAppearance extends AnimateDisappearance {
+
+        final LoggingInfo postInfo;
+
+        public AnimateAppearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+                LoggingInfo post) {
+            super(viewHolder, pre);
+            this.postInfo = post;
+        }
+    }
+
+    static class AnimateDisappearance extends AnimateLogBase {
+        public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+            super(viewHolder, pre);
+        }
+    }
+    static class AnimateLogBase {
+
+        final RecyclerView.ViewHolder viewHolder;
+        final LoggingInfo preInfo;
+
+        public AnimateLogBase(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+            this.viewHolder = viewHolder;
+            this.preInfo = pre;
+        }
+    }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 53c3f81..c5f3408 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -202,19 +202,23 @@
         mRecyclerView = null;
     }
 
-    void waitForAnimations(int seconds) throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(2);
-        boolean running = mRecyclerView.mItemAnimator
-                .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
-                    @Override
-                    public void onAnimationsFinished() {
-                        latch.countDown();
-                    }
-                });
-        if (running) {
-            latch.countDown();
-            latch.await(seconds, TimeUnit.SECONDS);
-        }
+    void waitForAnimations(int seconds) throws Throwable {
+        final CountDownLatch latch = new CountDownLatch(1);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.mItemAnimator
+                        .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
+                            @Override
+                            public void onAnimationsFinished() {
+                                latch.countDown();
+                            }
+                        });
+            }
+        });
+
+        assertTrue("animations didn't finish on expected time of " + seconds + " seconds",
+                latch.await(seconds, TimeUnit.SECONDS));
     }
 
     public boolean requestFocus(final View view) {
@@ -548,6 +552,7 @@
         int mAdapterIndex;
 
         final String mText;
+        int mType = 0;
 
         Item(int adapterIndex, String text) {
             mAdapterIndex = adapterIndex;
@@ -584,6 +589,11 @@
         }
 
         @Override
+        public int getItemViewType(int position) {
+            return getItemAt(position).mType;
+        }
+
+        @Override
         public void onViewAttachedToWindow(TestViewHolder holder) {
             super.onViewAttachedToWindow(holder);
             mAttachmentCounter.onViewAttached(holder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
index c9192a7..0e4bca7 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
@@ -440,7 +440,7 @@
         @Override
         public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
                 ItemHolderInfo preInfo) {
-            animateDisappearanceList.add(new AnimateDisappearance(viewHolder, preInfo));
+            animateDisappearanceList.add(new AnimateDisappearance(viewHolder, (LoggingInfo) preInfo));
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             dispatchAnimationFinished(viewHolder);
 
@@ -451,7 +451,7 @@
         public boolean animateAppearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
             animateAppearanceList.add(
-                    new AnimateAppearance(viewHolder, preInfo, postInfo));
+                    new AnimateAppearance(viewHolder, (LoggingInfo) preInfo, (LoggingInfo) postInfo));
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             assertSame(postLayoutInfo.get(viewHolder), postInfo);
             dispatchAnimationFinished(viewHolder);
@@ -462,7 +462,8 @@
         public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
                 ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
-            animatePersistenceList.add(new AnimatePersistence(viewHolder, preInfo, postInfo));
+            animatePersistenceList.add(new AnimatePersistence(viewHolder, (LoggingInfo) preInfo,
+                    (LoggingInfo) postInfo));
             dispatchAnimationFinished(viewHolder);
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             assertSame(postLayoutInfo.get(viewHolder), postInfo);
@@ -473,7 +474,8 @@
         public boolean animateChange(RecyclerView.ViewHolder oldHolder,
                 RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
-            animateChangeList.add(new AnimateChange(oldHolder, newHolder, preInfo, postInfo));
+            animateChangeList.add(new AnimateChange(oldHolder, newHolder, (LoggingInfo) preInfo,
+                    (LoggingInfo) postInfo));
             if (oldHolder != null) {
                 dispatchAnimationFinished(oldHolder);
                 assertSame(preLayoutInfo.get(oldHolder), preInfo);
@@ -506,65 +508,6 @@
         }
     }
 
-    static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
-
-        final RecyclerView.ViewHolder viewHolder;
-        @RecyclerView.ItemAnimator.AdapterChanges
-        final int changeFlags;
-        final List<Object> payloads;
-
-        LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
-            this.viewHolder = viewHolder;
-            this.changeFlags = changeFlags;
-            if (payloads != null) {
-                this.payloads = new ArrayList<>();
-                this.payloads.addAll(payloads);
-            } else {
-                this.payloads = null;
-            }
-            setFrom(viewHolder);
-        }
-    }
-
-    static class AnimateChange extends AnimatePersistence {
-
-        final RecyclerView.ViewHolder newHolder;
-
-        public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
-                Object pre, Object post) {
-            super(oldHolder, pre, post);
-            this.newHolder = newHolder;
-        }
-    }
-
-    static class AnimatePersistence extends AnimateAppearance {
-
-        public AnimatePersistence(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
-            super(viewHolder, pre, post);
-        }
-    }
-
-    static class AnimateAppearance extends AnimateDisappearance {
-
-        final LoggingInfo postInfo;
-
-        public AnimateAppearance(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
-            super(viewHolder, pre);
-            this.postInfo = (LoggingInfo) post;
-        }
-    }
-
-    static class AnimateDisappearance {
-
-        final RecyclerView.ViewHolder viewHolder;
-        final LoggingInfo preInfo;
-
-        public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, Object pre) {
-            this.viewHolder = viewHolder;
-            this.preInfo = (LoggingInfo) pre;
-        }
-    }
-
     interface CanReUseCallback {
 
         boolean canReUse(RecyclerView.ViewHolder viewHolder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
index 04031d4..e41286e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
@@ -14,6 +14,7 @@
 package android.support.v7.widget;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -29,8 +30,59 @@
 
     final ArrayList<RecyclerView.ViewHolder> mChangeNewVHs = new ArrayList<RecyclerView.ViewHolder>();
 
+    List<BaseRecyclerViewAnimationsTest.AnimateAppearance> mAnimateAppearanceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimateDisappearance> mAnimateDisappearanceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimatePersistence> mAnimatePersistenceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimateChange> mAnimateChangeList = new ArrayList<>();
+
     CountDownLatch mWaitForPendingAnimations;
 
+    public boolean contains(RecyclerView.ViewHolder viewHolder,
+            List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
+        for (BaseRecyclerViewAnimationsTest.AnimateLogBase log : list) {
+            if (log.viewHolder == viewHolder) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preLayoutInfo) {
+        mAnimateDisappearanceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateDisappearance(viewHolder, null));
+        return super.animateDisappearance(viewHolder, preLayoutInfo);
+    }
+
+    @Override
+    public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preLayoutInfo,
+            ItemHolderInfo postLayoutInfo) {
+        mAnimateAppearanceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateAppearance(viewHolder, null, null));
+        return super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo);
+    }
+
+    @Override
+    public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preInfo,
+            ItemHolderInfo postInfo) {
+        mAnimatePersistenceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimatePersistence(viewHolder, null, null));
+        return super.animatePersistence(viewHolder, preInfo, postInfo);
+    }
+
+    @Override
+    public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+            RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
+            ItemHolderInfo postInfo) {
+        mAnimateChangeList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateChange(oldHolder, newHolder, null,
+                        null));
+        return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
+    }
+
     @Override
     public void runPendingAnimations() {
         if (mWaitForPendingAnimations != null) {
@@ -84,5 +136,9 @@
         mMoveVHs.clear();
         mChangeOldVHs.clear();
         mChangeNewVHs.clear();
+        mAnimateChangeList.clear();
+        mAnimatePersistenceList.clear();
+        mAnimateAppearanceList.clear();
+        mAnimateDisappearanceList.clear();
     }
 }
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 85b6a5f..9887d99 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -17,6 +17,7 @@
 package android.support.v7.widget;
 
 import android.graphics.Rect;
+import android.os.Debug;
 import android.support.v4.view.ViewCompat;
 import android.util.Log;
 import android.view.View;
@@ -35,6 +36,310 @@
  */
 public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest {
 
+    final List<TestViewHolder> recycledVHs = new ArrayList<>();
+
+    public void testDontLayoutReusedViewWithoutPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    final TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        fail("pre layout is not expected");
+                    }
+
+                    @Override
+                    void beforePostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 7;
+                        View targetView = recycler
+                                .getViewForPosition(target.getAdapterPosition());
+                        assertSame(targetView, target.itemView);
+                        super.beforePostLayout(recycler, layoutManager, state);
+                    }
+
+                    @Override
+                    void afterPostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        super.afterPostLayout(recycler, layoutManager, state);
+                        assertNull("test sanity. this view should not be re-laid out in post "
+                                + "layout", target.itemView.getParent());
+                    }
+                };
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.requestSimpleAnimationsInNextLayout();
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                checkForMainThreadException();
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                // This is a LayoutManager problem if it asked for the view but didn't properly
+                // lay it out. It will move to disappearance
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontLayoutReusedViewWithPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    final TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        mLayoutItemCount = 9;
+                        super.beforePreLayout(recycler, lm, state);
+                    }
+
+                    @Override
+                    void beforePostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 7;
+                        super.beforePostLayout(recycler, layoutManager, state);
+                    }
+
+                    @Override
+                    void afterPostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        super.afterPostLayout(recycler, layoutManager, state);
+                        assertNull("test sanity. this view should not be re-laid out in post "
+                                + "layout", target.itemView.getParent());
+                    }
+                };
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(1, 1);
+                mLayoutManager.waitForLayout(2);
+                checkForMainThreadException();
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                // This is a LayoutManager problem if it asked for the view but didn't properly
+                // lay it out. It will move to disappearance.
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithoutPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.requestSimpleAnimationsInNextLayout();
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithoutAnimations() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(2, 1);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                // This LM does not layout the additional item so it does predictive wrong.
+                // We should still handle it and animate persistence for this item
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(itemAnimator.mMoveVHs.contains(target));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithProperPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        mLayoutItemCount = 9;
+                        super.beforePreLayout(recycler, lm, state);
+                    }
+
+                    @Override
+                    void afterPreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 8;
+                        super.afterPreLayout(recycler, layoutManager, state);
+                    }
+                };
+
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(2, 1);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                // This LM implements predictive animations properly by requesting target view
+                // in pre-layout.
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(itemAnimator.mMoveVHs.contains(target));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontReuseHiddenViewOnInvalidate() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mTestAdapter.dispatchDataSetChanged();
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertFalse(mRecyclerView.getItemAnimator().isRunning());
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontReuseOnTypeChange() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                target.mBoundItem.mType += 2;
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1);
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+
+                assertTrue(itemAnimator.mChangeOldVHs.contains(target));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(mRecyclerView.mChildHelper.isHidden(target.itemView));
+                assertFalse(recycledVHs.contains(target));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    interface ReuseTestCallback {
+
+        void postSetup(List<TestViewHolder> recycledList, TestViewHolder target) throws Throwable;
+    }
+
+    @Override
+    protected RecyclerView.ItemAnimator createItemAnimator() {
+        return new LoggingItemAnimator();
+    }
+
+    public void reuseHiddenViewTest(ReuseTestCallback callback) throws Throwable {
+        TestAdapter adapter = new TestAdapter(10) {
+            @Override
+            public void onViewRecycled(TestViewHolder holder) {
+                super.onViewRecycled(holder);
+                recycledVHs.add(holder);
+            }
+        };
+        setupBasic(10, 0, 10, adapter);
+        mRecyclerView.setItemViewCacheSize(0);
+        TestViewHolder target = (TestViewHolder) mRecyclerView.findViewHolderForAdapterPosition(9);
+        mRecyclerView.getItemAnimator().setAddDuration(1000);
+        mRecyclerView.getItemAnimator().setRemoveDuration(1000);
+        mRecyclerView.getItemAnimator().setChangeDuration(1000);
+        mRecyclerView.getItemAnimator().setMoveDuration(1000);
+        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
+        mLayoutManager.expectLayouts(2);
+        adapter.deleteAndNotify(2, 1);
+        mLayoutManager.waitForLayout(2);
+        // test sanity, make sure target is hidden now
+        assertTrue("test sanity", mRecyclerView.mChildHelper.isHidden(target.itemView));
+        callback.postSetup(recycledVHs, target);
+        // TODO TEST ITEM INVALIDATION OR TYPE CHANGE IN BETWEEN
+        // TODO TEST ITEM IS RECEIVED FROM RECYCLER BUT NOT RE-ADDED
+        // TODO TEST ITEM ANIMATOR IS CALLED TO GET NEW INFORMATION ABOUT LOCATION
+
+    }
+
     public void testDetachBeforeAnimations() throws Throwable {
         setupBasic(10, 0, 5);
         final RecyclerView rv = mRecyclerView;
@@ -76,7 +381,7 @@
                 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
                     @Override
                     public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
-                                               RecyclerView.State state) {
+                            RecyclerView.State state) {
                         if (view == targetChild[0]) {
                             outRect.set(10, 20, 30, 40);
                         } else {
@@ -230,7 +535,6 @@
         mLayoutManager.waitForLayout(2);
 
 
-
     }
 
     public void testAddRemoveSamePass() throws Throwable {
@@ -326,7 +630,7 @@
         changeAnimTest(false, false, true, false);
     }
 
-    public void testChangeAnimations()  throws Throwable {
+    public void testChangeAnimations() throws Throwable {
         final boolean[] booleans = {true, false};
         for (boolean supportsChange : booleans) {
             for (boolean changeType : booleans) {
@@ -341,7 +645,7 @@
     }
 
     public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
-            final boolean hasStableIds, final boolean deleteSomeItems)  throws Throwable {
+            final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
         final int changedIndex = 3;
         final int defaultType = 1;
         final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
@@ -376,7 +680,7 @@
         };
         testAdapter.setHasStableIds(hasStableIds);
         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
-        ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
                 supportsChangeAnim);
 
         final RecyclerView.ViewHolder toBeChangedVH =
@@ -435,7 +739,7 @@
         if (list1.size() != list2.size()) {
             return false;
         }
-        for (int i= 0; i < list1.size(); i++) {
+        for (int i = 0; i < list1.size(); i++) {
             if (!list1.get(i).equals(list2.get(i))) {
                 return false;
             }
@@ -444,8 +748,8 @@
     }
 
     private void testChangeWithPayload(final boolean supportsChangeAnim,
-            Object[][] notifyPayloads,  Object[][] expectedPayloadsInOnBind)
-                    throws Throwable {
+            Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
+            throws Throwable {
         final List<Object> expectedPayloads = new ArrayList<Object>();
         final int changedIndex = 3;
         TestAdapter testAdapter = new TestAdapter(10) {
@@ -476,11 +780,11 @@
         };
         testAdapter.setHasStableIds(false);
         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
-        ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
                 supportsChangeAnim);
 
         int numTests = notifyPayloads.length;
-        for (int i= 0; i < numTests; i++) {
+        for (int i = 0; i < numTests; i++) {
             mLayoutManager.expectLayouts(1);
             expectedPayloads.clear();
             for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
@@ -496,45 +800,46 @@
                 }
             });
             mLayoutManager.waitForLayout(2);
+            checkForMainThreadException();
         }
     }
 
-    public void testCrossFadingChangeAnimationWithPayload()  throws Throwable {
+    public void testCrossFadingChangeAnimationWithPayload() throws Throwable {
         // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
         testChangeWithPayload(true,
                 new Object[][]{
-                    new Object[]{"abc"},
-                    new Object[]{"abc", null, "cdf"},
-                    new Object[]{"abc", null},
-                    new Object[]{null, "abc"},
-                    new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[]{"abc", null, "cdf"},
+                        new Object[]{"abc", null},
+                        new Object[]{null, "abc"},
+                        new Object[]{"abc", "cdf"}
                 },
                 new Object[][]{
-                    new Object[0],
-                    new Object[0],
-                    new Object[0],
-                    new Object[0],
-                    new Object[0]
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[0]
                 });
     }
 
-    public void testNoChangeAnimationWithPayload()  throws Throwable {
+    public void testNoChangeAnimationWithPayload() throws Throwable {
         // for Change Animation disabled, payload should match the payloads unless
         // null payload is fired.
         testChangeWithPayload(false,
                 new Object[][]{
-                    new Object[]{"abc"},
-                    new Object[]{"abc", null, "cdf"},
-                    new Object[]{"abc", null},
-                    new Object[]{null, "abc"},
-                    new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[]{"abc", null, "cdf"},
+                        new Object[]{"abc", null},
+                        new Object[]{null, "abc"},
+                        new Object[]{"abc", "cdf"}
                 },
                 new Object[][]{
-                new Object[]{"abc"},
-                new Object[0],
-                new Object[0],
-                new Object[0],
-                new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[]{"abc", "cdf"}
                 });
     }
 
@@ -570,7 +875,7 @@
         });
 
         // now keep adding children to trigger more children being created etc.
-        for (int i = 0; i < 100; i ++) {
+        for (int i = 0; i < 100; i++) {
             adapter.addAndNotify(15, 1);
             Thread.sleep(50);
         }
@@ -618,19 +923,19 @@
         adapter.setHasStableIds(true);
         initialSet.addAll(adapter.mItems);
         positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter testAdapter) throws Throwable {
-                Item item5 = adapter.mItems.get(5);
-                Item item6 = adapter.mItems.get(6);
-                item5.mAdapterIndex = 6;
-                item6.mAdapterIndex = 5;
-                adapter.mItems.remove(5);
-                adapter.mItems.add(6, item5);
-                adapter.dispatchDataSetChanged();
-                //hacky, we support only 1 layout pass
-                mLayoutManager.layoutLatch.countDown();
-            }
-        }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
+                    @Override
+                    void onRun(TestAdapter testAdapter) throws Throwable {
+                        Item item5 = adapter.mItems.get(5);
+                        Item item6 = adapter.mItems.get(6);
+                        item5.mAdapterIndex = 6;
+                        item6.mAdapterIndex = 5;
+                        adapter.mItems.remove(5);
+                        adapter.mItems.add(6, item5);
+                        adapter.dispatchDataSetChanged();
+                        //hacky, we support only 1 layout pass
+                        mLayoutManager.layoutLatch.countDown();
+                    }
+                }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
                 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
                 PositionConstraint.scrap(9, -1, 9));
         // now mix items.
@@ -682,7 +987,7 @@
                     itemViewTypeQueries.contains(i));
             if (adapter.hasStableIds()) {
                 assertTrue("getItemId for existing item " + i
-                        + " should be called when adapter has stable ids",
+                                + " should be called when adapter has stable ids",
                         itemIdQueries.contains(i));
             }
         }
@@ -1035,35 +1340,35 @@
 
     public void testAddDelete2() throws Throwable {
         positionStatesTest(5, 0, 5, new AdapterOps() {
-            // 0 1 2 3 4
-            // 0 1 2 a b 3 4
-            // 0 1 b 3 4
-            // pre: 0 1 2 3 4
-            // pre w/ adap: 0 1 2 b 3 4
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
-            }
-        }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
+                    // 0 1 2 3 4
+                    // 0 1 2 a b 3 4
+                    // 0 1 b 3 4
+                    // pre: 0 1 2 3 4
+                    // pre w/ adap: 0 1 2 b 3 4
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
+                    }
+                }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
                 PositionConstraint.scrap(3, 3, 3)
         );
     }
 
     public void testAddDelete1() throws Throwable {
         positionStatesTest(5, 0, 5, new AdapterOps() {
-            // 0 1 2 3 4
-            // 0 1 2 a b 3 4
-            // 0 2 a b 3 4
-            // 0 c d 2 a b 3 4
-            // 0 c d 2 a 4
-            // c d 2 a 4
-            // pre: 0 1 2 3 4
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
-                        new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
-            }
-        }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
+                    // 0 1 2 3 4
+                    // 0 1 2 a b 3 4
+                    // 0 2 a b 3 4
+                    // 0 c d 2 a b 3 4
+                    // 0 c d 2 a 4
+                    // c d 2 a 4
+                    // pre: 0 1 2 3 4
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
+                                new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
+                    }
+                }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
                 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
                 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
                 PositionConstraint.adapter(1), PositionConstraint.adapter(3)
@@ -1072,12 +1377,12 @@
 
     public void testAddSameIndexTwice() throws Throwable {
         positionStatesTest(12, 2, 7, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
-                        new int[]{11, 1});
-            }
-        }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
+                                new int[]{11, 1});
+                    }
+                }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
                 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
                 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
                 PositionConstraint.scrap(8, 8, 13)
@@ -1086,12 +1391,12 @@
 
     public void testDeleteTwice() throws Throwable {
         positionStatesTest(12, 2, 7, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
-                        new int[]{0, 1});// delete item ids 0,2,9,1
-            }
-        }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
+                                new int[]{0, 1});// delete item ids 0,2,9,1
+                    }
+                }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
                 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
                 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
                 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
@@ -1103,10 +1408,11 @@
             int firstLayoutItemCount, AdapterOps adapterChanges,
             final PositionConstraint... constraints) throws Throwable {
         positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
-                adapterChanges,  constraints);
+                adapterChanges, constraints);
     }
+
     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
-            int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
+            int firstLayoutItemCount, TestAdapter adapter, AdapterOps adapterChanges,
             final PositionConstraint... constraints) throws Throwable {
         setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
         mLayoutManager.expectLayouts(2);
@@ -1125,7 +1431,8 @@
                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
                 StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
                 for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
-                    positionLog.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
+                    positionLog.append(entry.getKey()).append(":").append(entry.getValue())
+                            .append("\n");
                 }
                 for (PositionConstraint constraint : constraints) {
                     if (constraint.mPreLayoutPos != -1) {
@@ -1170,7 +1477,8 @@
     public void testAddThenRecycleRemovedView() throws Throwable {
         setupBasic(10);
         final AtomicInteger step = new AtomicInteger(0);
-        final List<RecyclerView.ViewHolder> animateRemoveList = new ArrayList<RecyclerView.ViewHolder>();
+        final List<RecyclerView.ViewHolder> animateRemoveList
+                = new ArrayList<RecyclerView.ViewHolder>();
         DefaultItemAnimator animator = new DefaultItemAnimator() {
             @Override
             public boolean animateRemove(RecyclerView.ViewHolder holder) {