Merge "Revert "Add EXTRA_STREAM_TYPE"" into nyc-support-25.4-dev
diff --git a/api/current.txt b/api/current.txt
index 665bc78..eb66483 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2636,7 +2636,7 @@
     ctor public PlaybackControlGlue(android.content.Context, int[], int[]);
     method public void enableProgressUpdating(boolean);
     method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
-    method public android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
+    method public deprecated android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
     method public abstract int getCurrentPosition();
     method public abstract int getCurrentSpeedId();
     method public int[] getFastForwardSpeeds();
@@ -2644,6 +2644,7 @@
     method public abstract int getMediaDuration();
     method public abstract java.lang.CharSequence getMediaSubtitle();
     method public abstract java.lang.CharSequence getMediaTitle();
+    method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
     method public int[] getRewindSpeeds();
     method public abstract long getSupportedActions();
     method public int getUpdatePeriod();
@@ -2660,8 +2661,9 @@
     method public void play(int);
     method public final void play();
     method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
-    method public void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
+    method public deprecated void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
     method public void setFadingEnabled(boolean);
+    method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
     method public void updateProgress();
     field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
     field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
@@ -4575,7 +4577,6 @@
     method public final android.support.v4.app.FragmentManager getFragmentManager();
     method public final java.lang.Object getHost();
     method public final int getId();
-    method public android.view.LayoutInflater getLayoutInflater(android.os.Bundle);
     method public android.support.v4.app.LoaderManager getLoaderManager();
     method public final android.support.v4.app.Fragment getParentFragment();
     method public java.lang.Object getReenterTransition();
@@ -4618,6 +4619,7 @@
     method public void onDestroyOptionsMenu();
     method public void onDestroyView();
     method public void onDetach();
+    method public android.view.LayoutInflater onGetLayoutInflater(android.os.Bundle);
     method public void onHiddenChanged(boolean);
     method public void onInflate(android.content.Context, android.util.AttributeSet, android.os.Bundle);
     method public deprecated void onInflate(android.app.Activity, android.util.AttributeSet, android.os.Bundle);
@@ -4778,7 +4780,7 @@
     method public abstract android.support.v4.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
     method public abstract int getBackStackEntryCount();
     method public abstract android.support.v4.app.Fragment getFragment(android.os.Bundle, java.lang.String);
-    method public abstract java.util.List<android.support.v4.app.Fragment> getFragments();
+    method public abstract java.util.Collection<android.support.v4.app.Fragment> getFragments();
     method public abstract boolean isDestroyed();
     method public abstract void popBackStack();
     method public abstract void popBackStack(java.lang.String, int);
@@ -8109,6 +8111,13 @@
     field public static final int INVALID_ID = -2147483648; // 0x80000000
   }
 
+  public class ImageViewCompat {
+    method public static android.content.res.ColorStateList getImageTintList(android.widget.ImageView);
+    method public static android.graphics.PorterDuff.Mode getImageTintMode(android.widget.ImageView);
+    method public static void setImageTintList(android.widget.ImageView, android.content.res.ColorStateList);
+    method public static void setImageTintMode(android.widget.ImageView, android.graphics.PorterDuff.Mode);
+  }
+
   public final class ListPopupWindowCompat {
     method public static android.view.View.OnTouchListener createDragToOpenListener(java.lang.Object, android.view.View);
   }
diff --git a/compat/api21/android/support/v4/view/ViewCompatLollipop.java b/compat/api21/android/support/v4/view/ViewCompatLollipop.java
index 26c462a..f38fec0 100644
--- a/compat/api21/android/support/v4/view/ViewCompatLollipop.java
+++ b/compat/api21/android/support/v4/view/ViewCompatLollipop.java
@@ -16,13 +16,13 @@
 
 package android.support.v4.view;
 
+import android.annotation.TargetApi;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.support.annotation.RequiresApi;
-import android.annotation.TargetApi;
 import android.view.View;
 import android.view.ViewParent;
 import android.view.WindowInsets;
diff --git a/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java b/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java
new file mode 100644
index 0000000..c5279d0
--- /dev/null
+++ b/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.widget.ImageView;
+
+@RequiresApi(21)
+class ImageViewCompatLollipop {
+    static ColorStateList getImageTintList(ImageView view) {
+        return view.getImageTintList();
+    }
+
+    static void setImageTintList(ImageView view, ColorStateList tintList) {
+        view.setImageTintList(tintList);
+
+        if (Build.VERSION.SDK_INT == 21) {
+            // Work around a bug in L that did not update the state of the image source
+            // after applying the tint
+            Drawable imageViewDrawable = view.getDrawable();
+            boolean hasTint = (view.getImageTintList() != null)
+                    && (view.getImageTintMode() != null);
+            if ((imageViewDrawable != null) && hasTint) {
+                if (imageViewDrawable.isStateful()) {
+                    imageViewDrawable.setState(view.getDrawableState());
+                }
+                view.setImageDrawable(imageViewDrawable);
+            }
+        }
+    }
+
+    static PorterDuff.Mode getImageTintMode(ImageView view) {
+        return view.getImageTintMode();
+    }
+
+    static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+        view.setImageTintMode(mode);
+
+        if (Build.VERSION.SDK_INT == 21) {
+            // Work around a bug in L that did not update the state of the image source
+            // after applying the tint
+            Drawable imageViewDrawable = view.getDrawable();
+            boolean hasTint = (view.getImageTintList() != null)
+                    && (view.getImageTintMode() != null);
+            if ((imageViewDrawable != null) && hasTint) {
+                if (imageViewDrawable.isStateful()) {
+                    imageViewDrawable.setState(view.getDrawableState());
+                }
+                view.setImageDrawable(imageViewDrawable);
+            }
+        }
+    }
+}
diff --git a/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java b/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
index 83c014b..9a22d7e 100644
--- a/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
+++ b/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
@@ -22,7 +22,7 @@
 
 /**
  * Interface which allows a {@link android.view.View} to receive background tinting calls from
- * {@code ViewCompat} when running on API v20 devices or lower.
+ * {@link ViewCompat} when running on API v20 devices or lower.
  */
 public interface TintableBackgroundView {
 
diff --git a/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java b/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java
new file mode 100644
index 0000000..1963340
--- /dev/null
+++ b/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.widget.ImageView;
+
+class ImageViewCompatBase {
+    static ColorStateList getImageTintList(ImageView view) {
+        return (view instanceof TintableImageSourceView)
+                ? ((TintableImageSourceView) view).getSupportImageTintList()
+                : null;
+    }
+
+    static void setImageTintList(ImageView view, ColorStateList tintList) {
+        if (view instanceof TintableImageSourceView) {
+            ((TintableImageSourceView) view).setSupportImageTintList(tintList);
+        }
+    }
+
+    static PorterDuff.Mode getImageTintMode(ImageView view) {
+        return (view instanceof TintableImageSourceView)
+                ? ((TintableImageSourceView) view).getSupportImageTintMode()
+                : null;
+    }
+
+    static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+        if (view instanceof TintableImageSourceView) {
+            ((TintableImageSourceView) view).setSupportImageTintMode(mode);
+        }
+    }
+}
diff --git a/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java b/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java
new file mode 100644
index 0000000..0c3d436
--- /dev/null
+++ b/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Interface which allows an {@link android.widget.ImageView} to receive image tinting calls
+ * from {@link ImageViewCompat} when running on API v20 devices or lower.
+ *
+ * @hide Internal use only
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface TintableImageSourceView {
+
+    /**
+     * Applies a tint to the image drawable. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to the source's image will automatically
+     * mutate the drawable and apply the specified tint and tint mode.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @see #getSupportImageTintList()
+     */
+    void setSupportImageTintList(@Nullable ColorStateList tint);
+
+    /**
+     * Return the tint applied to the image drawable, if specified.
+     *
+     * @return the tint applied to the image drawable
+     */
+    @Nullable
+    ColorStateList getSupportImageTintList();
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setSupportImageTintList(ColorStateList)}} to the image
+     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @see #getSupportImageTintMode()
+     */
+    void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode);
+
+    /**
+     * Return the blending mode used to apply the tint to the image
+     * drawable, if specified.
+     *
+     * @return the blending mode used to apply the tint to the image drawable
+     */
+    @Nullable
+    PorterDuff.Mode getSupportImageTintMode();
+}
diff --git a/compat/java/android/support/v4/view/ViewCompat.java b/compat/java/android/support/v4/view/ViewCompat.java
index 434b850..82ac508 100644
--- a/compat/java/android/support/v4/view/ViewCompat.java
+++ b/compat/java/android/support/v4/view/ViewCompat.java
@@ -3130,7 +3130,7 @@
      * Applies a tint to the background drawable.
      * <p>
      * This will always take effect when running on API v21 or newer. When running on platforms
-     * previous to API v21, it will only take effect if {@code view} implement the
+     * previous to API v21, it will only take effect if {@code view} implements the
      * {@code TintableBackgroundView} interface.
      */
     public static void setBackgroundTintList(View view, ColorStateList tintList) {
@@ -3160,6 +3160,7 @@
     public static void setBackgroundTintMode(View view, PorterDuff.Mode mode) {
         IMPL.setBackgroundTintMode(view, mode);
     }
+
     // TODO: getters for various view properties (rotation, etc)
 
     /**
diff --git a/compat/java/android/support/v4/widget/ImageViewCompat.java b/compat/java/android/support/v4/widget/ImageViewCompat.java
new file mode 100644
index 0000000..3c0b357
--- /dev/null
+++ b/compat/java/android/support/v4/widget/ImageViewCompat.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.widget.ImageView;
+
+/**
+ * Helper for accessing features in {@link ImageView} introduced in later platform releases
+ * in a backwards compatible fashion.
+ */
+public class ImageViewCompat {
+    interface ImageViewCompatImpl {
+        ColorStateList getImageTintList(ImageView view);
+
+        void setImageTintList(ImageView view, ColorStateList tintList);
+
+        PorterDuff.Mode getImageTintMode(ImageView view);
+
+        void setImageTintMode(ImageView view, PorterDuff.Mode mode);
+    }
+
+    static class BaseViewCompatImpl implements ImageViewCompatImpl {
+        @Override
+        public ColorStateList getImageTintList(ImageView view) {
+            return ImageViewCompatBase.getImageTintList(view);
+        }
+
+        @Override
+        public void setImageTintList(ImageView view, ColorStateList tintList) {
+            ImageViewCompatBase.setImageTintList(view, tintList);
+        }
+
+        @Override
+        public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+            ImageViewCompatBase.setImageTintMode(view, mode);
+        }
+
+        @Override
+        public PorterDuff.Mode getImageTintMode(ImageView view) {
+            return ImageViewCompatBase.getImageTintMode(view);
+        }
+    }
+
+    static class LollipopViewCompatImpl extends BaseViewCompatImpl {
+        @Override
+        public ColorStateList getImageTintList(ImageView view) {
+            return ImageViewCompatLollipop.getImageTintList(view);
+        }
+
+        @Override
+        public void setImageTintList(ImageView view, ColorStateList tintList) {
+            ImageViewCompatLollipop.setImageTintList(view, tintList);
+        }
+
+        @Override
+        public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+            ImageViewCompatLollipop.setImageTintMode(view, mode);
+        }
+
+        @Override
+        public PorterDuff.Mode getImageTintMode(ImageView view) {
+            return ImageViewCompatLollipop.getImageTintMode(view);
+        }
+    }
+
+    static final ImageViewCompatImpl IMPL;
+    static {
+        if (android.os.Build.VERSION.SDK_INT >= 21) {
+            IMPL = new LollipopViewCompatImpl();
+        } else {
+            IMPL = new BaseViewCompatImpl();
+        }
+    }
+
+    /**
+     * Return the tint applied to the image drawable, if specified.
+     */
+    public static ColorStateList getImageTintList(ImageView view) {
+        return IMPL.getImageTintList(view);
+    }
+
+    /**
+     * Applies a tint to the image drawable.
+     */
+    public static void setImageTintList(ImageView view, ColorStateList tintList) {
+        IMPL.setImageTintList(view, tintList);
+    }
+
+    /**
+     * Return the blending mode used to apply the tint to the image drawable, if specified.
+     */
+    public static PorterDuff.Mode getImageTintMode(ImageView view) {
+        return IMPL.getImageTintMode(view);
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setImageTintList(android.widget.ImageView, android.content.res.ColorStateList)}
+     * to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     */
+    public static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+        IMPL.setImageTintMode(view, mode);
+    }
+
+    private ImageViewCompat() {}
+}
diff --git a/fragment/java/android/support/v4/app/DialogFragment.java b/fragment/java/android/support/v4/app/DialogFragment.java
index 6a6456b..dfd0416 100644
--- a/fragment/java/android/support/v4/app/DialogFragment.java
+++ b/fragment/java/android/support/v4/app/DialogFragment.java
@@ -302,9 +302,9 @@
     }
 
     @Override
-    public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+    public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
         if (!mShowsDialog) {
-            return super.getLayoutInflater(savedInstanceState);
+            return super.onGetLayoutInflater(savedInstanceState);
         }
 
         mDialog = onCreateDialog(savedInstanceState);
diff --git a/fragment/java/android/support/v4/app/Fragment.java b/fragment/java/android/support/v4/app/Fragment.java
index 698db20..f827cfb 100644
--- a/fragment/java/android/support/v4/app/Fragment.java
+++ b/fragment/java/android/support/v4/app/Fragment.java
@@ -1127,9 +1127,21 @@
      * a previous saved state, this is the state.
      * @return The LayoutInflater used to inflate Views of this Fragment.
      */
-    public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+    public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
+        // TODO: move the implementation in getLayoutInflater to here
+        return getLayoutInflater(savedInstanceState);
+    }
+
+    /**
+     * Use {@link #onGetLayoutInflater(Bundle)} instead
+     * @hide
+     * @deprecated Use {@link #onGetLayoutInflater(Bundle)} instead.
+     */
+    @Deprecated
+    @RestrictTo(LIBRARY_GROUP)
+    public LayoutInflater getLayoutInflater(Bundle savedFragmentState) {
         if (mHost == null) {
-            throw new IllegalStateException("getLayoutInflater() cannot be executed until the "
+            throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "
                     + "Fragment is attached to the FragmentManager.");
         }
         LayoutInflater result = mHost.onGetLayoutInflater();
diff --git a/fragment/java/android/support/v4/app/FragmentController.java b/fragment/java/android/support/v4/app/FragmentController.java
index 7894437..48b3602 100644
--- a/fragment/java/android/support/v4/app/FragmentController.java
+++ b/fragment/java/android/support/v4/app/FragmentController.java
@@ -29,7 +29,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -78,22 +77,14 @@
      * Returns the number of active fragments.
      */
     public int getActiveFragmentsCount() {
-        final List<Fragment> actives = mHost.mFragmentManager.mActive;
-        return actives == null ? 0 : actives.size();
+        return mHost.mFragmentManager.getActiveFragmentCount();
     }
 
     /**
      * Returns the list of active fragments.
      */
     public List<Fragment> getActiveFragments(List<Fragment> actives) {
-        if (mHost.mFragmentManager.mActive == null) {
-            return null;
-        }
-        if (actives == null) {
-            actives = new ArrayList<Fragment>(getActiveFragmentsCount());
-        }
-        actives.addAll(mHost.mFragmentManager.mActive);
-        return actives;
+        return mHost.mFragmentManager.getActiveFragments();
     }
 
     /**
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index f8839a7..bc6e842 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -62,6 +62,7 @@
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -328,14 +329,14 @@
     public abstract Fragment getFragment(Bundle bundle, String key);
 
     /**
-     * Get a list of all fragments that are currently added to the FragmentManager.
+     * Get a collection of all fragments that are currently added to the FragmentManager.
      * This may include those that are hidden as well as those that are shown.
      * This will not include any fragments only in the back stack, or fragments that
      * are detached or removed.
      *
-     * @return A list of all fragments that are added to the FragmentManager.
+     * @return A collection of all fragments that are added to the FragmentManager.
      */
-    public abstract List<Fragment> getFragments();
+    public abstract Collection<Fragment> getFragments();
 
     /**
      * Save the current instance state of the given Fragment.  This can be
@@ -546,6 +547,7 @@
     FragmentState[] mActive;
     int[] mAdded;
     BackStackState[] mBackStack;
+    int mNextFragmentIndex;
 
     public FragmentManagerState() {
     }
@@ -554,6 +556,7 @@
         mActive = in.createTypedArray(FragmentState.CREATOR);
         mAdded = in.createIntArray();
         mBackStack = in.createTypedArray(BackStackState.CREATOR);
+        mNextFragmentIndex = in.readInt();
     }
 
     @Override
@@ -566,6 +569,7 @@
         dest.writeTypedArray(mActive, flags);
         dest.writeIntArray(mAdded);
         dest.writeTypedArray(mBackStack, flags);
+        dest.writeInt(mNextFragmentIndex);
     }
 
     public static final Parcelable.Creator<FragmentManagerState> CREATOR
@@ -665,12 +669,12 @@
     }
 
     ArrayList<OpGenerator> mPendingActions;
-    Runnable[] mTmpActions;
     boolean mExecutingActions;
 
-    ArrayList<Fragment> mActive;
+    int mNextFragmentIndex = 0;
+
     ArrayList<Fragment> mAdded;
-    ArrayList<Integer> mAvailIndices;
+    SparseArray<Fragment> mActive;
     ArrayList<BackStackRecord> mBackStack;
     ArrayList<Fragment> mCreatedMenus;
 
@@ -829,6 +833,7 @@
         }
 
         doPendingDeferredStart();
+        burpActive();
         return executePop;
     }
 
@@ -872,10 +877,6 @@
         if (index == -1) {
             return null;
         }
-        if (index >= mActive.size()) {
-            throwException(new IllegalStateException("Fragment no longer exists for key "
-                    + key + ": index " + index));
-        }
         Fragment f = mActive.get(index);
         if (f == null) {
             throwException(new IllegalStateException("Fragment no longer exists for key "
@@ -885,15 +886,45 @@
     }
 
     @Override
-    public List<Fragment> getFragments() {
+    public Collection<Fragment> getFragments() {
         if (mAdded == null) {
             return Collections.EMPTY_LIST;
         }
         synchronized (mAdded) {
-            return (List<Fragment>) mAdded.clone();
+            return (Collection<Fragment>) mAdded.clone();
         }
     }
 
+    /**
+     * This is used by FragmentController to get the Active fragments.
+     *
+     * @return A list of active fragments in the fragment manager, including those that are in the
+     * back stack.
+     */
+    List<Fragment> getActiveFragments() {
+        if (mActive == null) {
+            return null;
+        }
+        final int count = mActive.size();
+        ArrayList<Fragment> fragments = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            fragments.add(mActive.valueAt(i));
+        }
+        return fragments;
+    }
+
+    /**
+     * Used by FragmentController to get the number of Active Fragments.
+     *
+     * @return The number of active fragments.
+     */
+    int getActiveFragmentCount() {
+        if (mActive == null) {
+            return 0;
+        }
+        return mActive.size();
+    }
+
     @Override
     public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
         if (fragment.mIndex < 0) {
@@ -939,7 +970,7 @@
                         writer.print(Integer.toHexString(System.identityHashCode(this)));
                         writer.println(":");
                 for (int i=0; i<N; i++) {
-                    Fragment f = mActive.get(i);
+                    Fragment f = mActive.valueAt(i);
                     writer.print(prefix); writer.print("  #"); writer.print(i);
                             writer.print(": "); writer.println(f);
                     if (f != null) {
@@ -1034,10 +1065,6 @@
             writer.print(prefix); writer.print("  mNoTransactionsBecause=");
                     writer.println(mNoTransactionsBecause);
         }
-        if (mAvailIndices != null && mAvailIndices.size() > 0) {
-            writer.print(prefix); writer.print("  mAvailIndices: ");
-                    writer.println(Arrays.toString(mAvailIndices.toArray()));
-        }
     }
 
     static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
@@ -1259,7 +1286,7 @@
                         // For fragments that are part of the content view
                         // layout, we need to instantiate the view immediately
                         // and the inflater will take care of adding it.
-                        f.mView = f.performCreateView(f.getLayoutInflater(
+                        f.mView = f.performCreateView(f.onGetLayoutInflater(
                                 f.mSavedFragmentState), null, f.mSavedFragmentState);
                         if (f.mView != null) {
                             f.mInnerView = f.mView;
@@ -1303,7 +1330,7 @@
                                 }
                             }
                             f.mContainer = container;
-                            f.mView = f.performCreateView(f.getLayoutInflater(
+                            f.mView = f.performCreateView(f.onGetLayoutInflater(
                                     f.mSavedFragmentState), container, f.mSavedFragmentState);
                             if (f.mView != null) {
                                 f.mInnerView = f.mView;
@@ -1610,7 +1637,7 @@
             // and detached.
             final int numActive = mActive.size();
             for (int i = 0; i < numActive; i++) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) {
                     moveFragmentToExpectedState(f);
                     if (f.mLoaderManager != null) {
@@ -1634,7 +1661,7 @@
         if (mActive == null) return;
 
         for (int i=0; i<mActive.size(); i++) {
-            Fragment f = mActive.get(i);
+            Fragment f = mActive.valueAt(i);
             if (f != null) {
                 performPendingDeferredStart(f);
             }
@@ -1646,17 +1673,11 @@
             return;
         }
 
-        if (mAvailIndices == null || mAvailIndices.size() <= 0) {
-            if (mActive == null) {
-                mActive = new ArrayList<Fragment>();
-            }
-            f.setIndex(mActive.size(), mParent);
-            mActive.add(f);
-
-        } else {
-            f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
-            mActive.set(f.mIndex, f);
+        f.setIndex(mNextFragmentIndex++, mParent);
+        if (mActive == null) {
+            mActive = new SparseArray<>();
         }
+        mActive.put(f.mIndex, f);
         if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
     }
 
@@ -1666,11 +1687,10 @@
         }
 
         if (DEBUG) Log.v(TAG, "Freeing fragment index " + f);
-        mActive.set(f.mIndex, null);
-        if (mAvailIndices == null) {
-            mAvailIndices = new ArrayList<Integer>();
-        }
-        mAvailIndices.add(f.mIndex);
+        // Don't remove yet. That happens in burpActive(). This prevents
+        // concurrent modification while iterating over mActive
+        mActive.put(f.mIndex, null);
+
         mHost.inactivateFragment(f.mWho);
         f.initState();
     }
@@ -1808,7 +1828,7 @@
         if (mActive != null) {
             // Now for any known fragment.
             for (int i=mActive.size()-1; i>=0; i--) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null && f.mFragmentId == id) {
                     return f;
                 }
@@ -1831,7 +1851,7 @@
         if (mActive != null && tag != null) {
             // Now for any known fragment.
             for (int i=mActive.size()-1; i>=0; i--) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null && tag.equals(f.mTag)) {
                     return f;
                 }
@@ -1843,7 +1863,7 @@
     public Fragment findFragmentByWho(String who) {
         if (mActive != null && who != null) {
             for (int i=mActive.size()-1; i>=0; i--) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null && (f=f.findFragmentByWho(who)) != null) {
                     return f;
                 }
@@ -1876,6 +1896,10 @@
         }
         synchronized (this) {
             if (mDestroyed || mHost == null) {
+                if (allowStateLoss) {
+                    // This FragmentManager isn't attached, so drop the entire transaction.
+                    return;
+                }
                 throw new IllegalStateException("Activity has been destroyed");
             }
             if (mPendingActions == null) {
@@ -1992,6 +2016,10 @@
     }
 
     public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
+        if (allowStateLoss && (mHost == null || mDestroyed)) {
+            // This FragmentManager isn't attached, so drop the entire transaction.
+            return;
+        }
         ensureExecReady(allowStateLoss);
         if (action.generateOps(mTmpRecords, mTmpIsPop)) {
             mExecutingActions = true;
@@ -2003,6 +2031,7 @@
         }
 
         doPendingDeferredStart();
+        burpActive();
     }
 
     /**
@@ -2033,6 +2062,7 @@
         }
 
         doPendingDeferredStart();
+        burpActive();
 
         return didSomething;
     }
@@ -2301,7 +2331,7 @@
             for (int i = 0; i < numActive; i++) {
                 // Allow added fragments to be removed during the pop since we aren't going
                 // to move them to the final state with moveToState(mCurState).
-                Fragment fragment = mActive.get(i);
+                Fragment fragment = mActive.valueAt(i);
                 if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded
                         && record.interactsWith(fragment.mContainerId)) {
                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
@@ -2423,7 +2453,7 @@
     private void endAnimatingAwayFragments() {
         final int numFragments = mActive == null ? 0 : mActive.size();
         for (int i = 0; i < numFragments; i++) {
-            Fragment fragment = mActive.get(i);
+            Fragment fragment = mActive.valueAt(i);
             if (fragment != null && fragment.getAnimatingAway() != null) {
                 // Give up waiting for the animation and just end it.
                 final int stateAfterAnimating = fragment.getStateAfterAnimating();
@@ -2470,7 +2500,7 @@
         if (mHavePendingDeferredStart) {
             boolean loadersRunning = false;
             for (int i = 0; i < mActive.size(); i++) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null && f.mLoaderManager != null) {
                     loadersRunning |= f.mLoaderManager.hasRunningLoaders();
                 }
@@ -2560,7 +2590,7 @@
         ArrayList<FragmentManagerNonConfig> childFragments = null;
         if (mActive != null) {
             for (int i=0; i<mActive.size(); i++) {
-                Fragment f = mActive.get(i);
+                Fragment f = mActive.valueAt(i);
                 if (f != null) {
                     if (f.mRetainInstance) {
                         if (fragments == null) {
@@ -2676,7 +2706,7 @@
         FragmentState[] active = new FragmentState[N];
         boolean haveFragments = false;
         for (int i=0; i<N; i++) {
-            Fragment f = mActive.get(i);
+            Fragment f = mActive.valueAt(i);
             if (f != null) {
                 if (f.mIndex < 0) {
                     throwException(new IllegalStateException(
@@ -2762,6 +2792,7 @@
         fms.mActive = active;
         fms.mAdded = added;
         fms.mBackStack = backStack;
+        fms.mNextFragmentIndex = mNextFragmentIndex;
         return fms;
     }
 
@@ -2783,7 +2814,15 @@
             for (int i = 0; i < count; i++) {
                 Fragment f = nonConfigFragments.get(i);
                 if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
-                FragmentState fs = fms.mActive[f.mIndex];
+                int index = 0; // index into fms.mActive
+                while (index < fms.mActive.length && fms.mActive[index].mIndex != f.mIndex) {
+                    index++;
+                }
+                if (index == fms.mActive.length) {
+                    throwException(new IllegalStateException("Could not find active fragment "
+                            + "with index " + f.mIndex));
+                }
+                FragmentState fs = fms.mActive[index];
                 fs.mInstance = f;
                 f.mSavedViewState = null;
                 f.mBackStackNesting = 0;
@@ -2801,10 +2840,7 @@
 
         // Build the full list of active fragments, instantiating them from
         // their saved state.
-        mActive = new ArrayList<>(fms.mActive.length);
-        if (mAvailIndices != null) {
-            mAvailIndices.clear();
-        }
+        mActive = new SparseArray<>(fms.mActive.length);
         for (int i=0; i<fms.mActive.length; i++) {
             FragmentState fs = fms.mActive[i];
             if (fs != null) {
@@ -2814,18 +2850,11 @@
                 }
                 Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
                 if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
-                mActive.add(f);
+                mActive.put(f.mIndex, f);
                 // Now that the fragment is instantiated (or came from being
                 // retained above), clear mInstance in case we end up re-restoring
                 // from this FragmentState again.
                 fs.mInstance = null;
-            } else {
-                mActive.add(null);
-                if (mAvailIndices == null) {
-                    mAvailIndices = new ArrayList<Integer>();
-                }
-                if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i);
-                mAvailIndices.add(i);
             }
         }
 
@@ -2836,12 +2865,10 @@
             for (int i = 0; i < count; i++) {
                 Fragment f = nonConfigFragments.get(i);
                 if (f.mTargetIndex >= 0) {
-                    if (f.mTargetIndex < mActive.size()) {
-                        f.mTarget = mActive.get(f.mTargetIndex);
-                    } else {
+                    f.mTarget = mActive.get(f.mTargetIndex);
+                    if (f.mTarget == null) {
                         Log.w(TAG, "Re-attaching retained fragment " + f
                                 + " target no longer exists: " + f.mTargetIndex);
-                        f.mTarget = null;
                     }
                 }
             }
@@ -2890,6 +2917,23 @@
         } else {
             mBackStack = null;
         }
+
+        this.mNextFragmentIndex = fms.mNextFragmentIndex;
+    }
+
+    /**
+     * To prevent list modification errors, mActive sets values to null instead of
+     * removing them when the Fragment becomes inactive. This cleans up the list at the
+     * end of executing the transactions.
+     */
+    private void burpActive() {
+        if (mActive != null) {
+            for (int i = mActive.size() - 1; i >= 0; i--) {
+                if (mActive.valueAt(i) == null) {
+                    mActive.delete(mActive.keyAt(i));
+                }
+            }
+        }
     }
 
     public void attachController(FragmentHostCallback host,
diff --git a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
index c9d7351..c6db19b 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
@@ -684,6 +684,52 @@
         assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
     }
 
+    /**
+     * When a fragment is saved in non-config, it should be restored to the same index.
+     */
+    @Test
+    @UiThreadTest
+    public void restoreNonConfig() throws Throwable {
+        FragmentController fc = FragmentTestUtil.createController(mActivityRule);
+        FragmentTestUtil.resume(mActivityRule, fc, null);
+        FragmentManager fm = fc.getSupportFragmentManager();
+
+        Fragment fragment1 = new StrictFragment();
+        fm.beginTransaction()
+                .add(fragment1, "1")
+                .addToBackStack(null)
+                .commit();
+        fm.executePendingTransactions();
+        Fragment fragment2 = new StrictFragment();
+        fragment2.setRetainInstance(true);
+        fragment2.setTargetFragment(fragment1, 0);
+        Fragment fragment3 = new StrictFragment();
+        fm.beginTransaction()
+                .remove(fragment1)
+                .add(fragment2, "2")
+                .add(fragment3, "3")
+                .addToBackStack(null)
+                .commit();
+        fm.executePendingTransactions();
+
+        Pair<Parcelable, FragmentManagerNonConfig> savedState =
+                FragmentTestUtil.destroy(mActivityRule, fc);
+
+        fc = FragmentTestUtil.createController(mActivityRule);
+        FragmentTestUtil.resume(mActivityRule, fc, savedState);
+        boolean foundFragment2 = false;
+        for (Fragment fragment : fc.getSupportFragmentManager().getFragments()) {
+            if (fragment == fragment2) {
+                foundFragment2 = true;
+                assertNotNull(fragment.getTargetFragment());
+                assertEquals("1", fragment.getTargetFragment().getTag());
+            } else {
+                assertNotEquals("2", fragment.getTag());
+            }
+        }
+        assertTrue(foundFragment2);
+    }
+
     private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
             int popExit) {
         FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index ff48420..9ee0c48 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -145,9 +145,10 @@
         }
     }
 
-    public static FragmentController createController(ActivityTestRule<FragmentTestActivity> rule) {
+    public static FragmentController createController(
+            ActivityTestRule<? extends FragmentActivity> rule) {
         final FragmentController[] controller = new FragmentController[1];
-        final FragmentTestActivity activity = rule.getActivity();
+        final FragmentActivity activity = rule.getActivity();
         runOnUiThreadRethrow(rule, new Runnable() {
             @Override
             public void run() {
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java b/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
index 932aba5..7179fd3 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
@@ -33,7 +33,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.List;
+import java.util.Collection;
 
 /**
  * Tests usage of the {@link FragmentTransaction} class.
@@ -179,9 +179,9 @@
                 .commit();
 
         FragmentTestUtil.executePendingTransactions(mActivityRule);
-        List<Fragment> fragments = fm.getFragments();
+        Collection<Fragment> fragments = fm.getFragments();
         assertEquals(1, fragments.size());
-        assertEquals(fragment, fragments.get(0));
+        assertTrue(fragments.contains(fragment));
 
         // Removed fragments shouldn't show
         fm.beginTransaction()
@@ -209,13 +209,13 @@
         FragmentTestUtil.executePendingTransactions(mActivityRule);
         fragments = fm.getFragments();
         assertEquals(1, fragments.size());
-        assertEquals(fragment, fragments.get(0));
+        assertTrue(fragments.contains(fragment));
 
         // And showing it again shouldn't change anything:
         FragmentTestUtil.popBackStackImmediate(mActivityRule);
         fragments = fm.getFragments();
         assertEquals(1, fragments.size());
-        assertEquals(fragment, fragments.get(0));
+        assertTrue(fragments.contains(fragment));
 
         // Now pop back to the start state
         FragmentTestUtil.popBackStackImmediate(mActivityRule);
@@ -223,8 +223,9 @@
         // We can't force concurrency, but we can do it lots of times and hope that
         // we hit it.
         for (int i = 0; i < 100; i++) {
+            Fragment fragment2 = new CorrectFragment();
             fm.beginTransaction()
-                    .add(R.id.content, fragment)
+                    .add(R.id.content, fragment2)
                     .addToBackStack(null)
                     .commit();
             getFragmentsUntilSize(1);
@@ -234,6 +235,49 @@
         }
     }
 
+    /**
+     * When a FragmentManager is detached, it should allow commitAllowingStateLoss()
+     * and commitNowAllowingStateLoss() by just dropping the transaction.
+     */
+    @Test
+    public void commitAllowStateLossDetached() throws Throwable {
+        Fragment fragment1 = new CorrectFragment();
+        mActivity.getSupportFragmentManager()
+                .beginTransaction()
+                .add(fragment1, "1")
+                .commit();
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+        final FragmentManager fm = fragment1.getChildFragmentManager();
+        mActivity.getSupportFragmentManager()
+                .beginTransaction()
+                .remove(fragment1)
+                .commit();
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+        assertEquals(0, mActivity.getSupportFragmentManager().getFragments().size());
+        assertEquals(0, fm.getFragments().size());
+
+        // Now the fragment1's fragment manager should allow commitAllowingStateLoss
+        // by doing nothing since it has been detached.
+        Fragment fragment2 = new CorrectFragment();
+        fm.beginTransaction()
+                .add(fragment2, "2")
+                .commitAllowingStateLoss();
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+        assertEquals(0, fm.getFragments().size());
+
+        // It should also allow commitNowAllowingStateLoss by doing nothing
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                Fragment fragment3 = new CorrectFragment();
+                fm.beginTransaction()
+                        .add(fragment3, "3")
+                        .commitNowAllowingStateLoss();
+                assertEquals(0, fm.getFragments().size());
+            }
+        });
+    }
+
     private void getFragmentsUntilSize(int expectedSize) {
         final long endTime = SystemClock.uptimeMillis() + 3000;
 
diff --git a/fragment/tests/java/android/support/v4/app/HostCallbacks.java b/fragment/tests/java/android/support/v4/app/HostCallbacks.java
index 9a0ef1c..15698a6 100644
--- a/fragment/tests/java/android/support/v4/app/HostCallbacks.java
+++ b/fragment/tests/java/android/support/v4/app/HostCallbacks.java
@@ -16,20 +16,19 @@
 package android.support.v4.app;
 
 import android.os.Handler;
-import android.support.v4.app.test.FragmentTestActivity;
 import android.view.LayoutInflater;
 import android.view.View;
 
-class HostCallbacks extends FragmentHostCallback<FragmentTestActivity> {
-    private final FragmentTestActivity mActivity;
+class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
+    private final FragmentActivity mActivity;
 
-    HostCallbacks(FragmentTestActivity activity, Handler handler, int windowAnimations) {
+    HostCallbacks(FragmentActivity activity, Handler handler, int windowAnimations) {
         super(activity, handler, windowAnimations);
         mActivity = activity;
     }
 
     @Override
-    public FragmentTestActivity onGetHost() {
+    public FragmentActivity onGetHost() {
         return mActivity;
     }
 
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index eef97f1..8fa2a30 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -849,10 +849,11 @@
 
     static class MediaBrowserImplBase
             implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
-        static final int CONNECT_STATE_DISCONNECTED = 0;
-        static final int CONNECT_STATE_CONNECTING = 1;
-        private static final int CONNECT_STATE_CONNECTED = 2;
-        static final int CONNECT_STATE_SUSPENDED = 3;
+        static final int CONNECT_STATE_DISCONNECTING = 0;
+        static final int CONNECT_STATE_DISCONNECTED = 1;
+        static final int CONNECT_STATE_CONNECTING = 2;
+        static final int CONNECT_STATE_CONNECTED = 3;
+        static final int CONNECT_STATE_SUSPENDED = 4;
 
         final Context mContext;
         final ComponentName mServiceComponent;
@@ -952,21 +953,26 @@
             // 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 (mCallbacksMessenger != null) {
-                try {
-                    mServiceBinderWrapper.disconnect(mCallbacksMessenger);
-                } 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);
+            mState = CONNECT_STATE_DISCONNECTING;
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mCallbacksMessenger != null) {
+                        try {
+                            mServiceBinderWrapper.disconnect(mCallbacksMessenger);
+                        } 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 (DEBUG) {
+                        Log.d(TAG, "disconnect...");
+                        dump();
+                    }
                 }
-            }
-            forceCloseConnection();
-
-            if (DEBUG) {
-                Log.d(TAG, "disconnect...");
-                dump();
-            }
+            });
         }
 
         /**
@@ -1047,7 +1053,7 @@
 
             // 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) {
+            if (isConnected()) {
                 try {
                     mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
                             mCallbacksMessenger);
@@ -1069,7 +1075,7 @@
             // Tell the service if necessary.
             try {
                 if (callback == null) {
-                    if (mState == CONNECT_STATE_CONNECTED) {
+                    if (isConnected()) {
                         mServiceBinderWrapper.removeSubscription(parentId, null,
                                 mCallbacksMessenger);
                     }
@@ -1078,7 +1084,7 @@
                     final List<Bundle> optionsList = sub.getOptionsList();
                     for (int i = callbacks.size() - 1; i >= 0; --i) {
                         if (callbacks.get(i) == callback) {
-                            if (mState == CONNECT_STATE_CONNECTED) {
+                            if (isConnected()) {
                                 mServiceBinderWrapper.removeSubscription(
                                         parentId, callback.mToken, mCallbacksMessenger);
                             }
@@ -1106,7 +1112,7 @@
             if (cb == null) {
                 throw new IllegalArgumentException("cb is null");
             }
-            if (mState != CONNECT_STATE_CONNECTED) {
+            if (!isConnected()) {
                 Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
                 mHandler.post(new Runnable() {
                     @Override
@@ -1264,6 +1270,8 @@
          */
         private static String getStateLabel(int state) {
             switch (state) {
+                case CONNECT_STATE_DISCONNECTING:
+                    return "CONNECT_STATE_DISCONNECTING";
                 case CONNECT_STATE_DISCONNECTED:
                     return "CONNECT_STATE_DISCONNECTED";
                 case CONNECT_STATE_CONNECTING:
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index b64d7e1..b7581f0 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -1219,7 +1219,6 @@
         public void writeToParcel(Parcel dest, int flags) {
             if (android.os.Build.VERSION.SDK_INT >= 21) {
                 dest.writeParcelable((Parcelable) mInner, flags);
-                dest.writeStrongBinder(mExtraBinder == null ? null : mExtraBinder.asBinder());
             } else {
                 dest.writeStrongBinder((IBinder) mInner);
             }
@@ -1278,14 +1277,12 @@
             @Override
             public Token createFromParcel(Parcel in) {
                 Object inner;
-                IMediaSession extraBinder = null;
                 if (android.os.Build.VERSION.SDK_INT >= 21) {
                     inner = in.readParcelable(null);
-                    extraBinder = IMediaSession.Stub.asInterface(in.readStrongBinder());
                 } else {
                     inner = in.readStrongBinder();
                 }
-                return new Token(inner, extraBinder);
+                return new Token(inner);
             }
 
             @Override
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index 93ead1e..e58817a 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -36,6 +36,12 @@
                 <action android:name="android.media.browse.MediaBrowserService"/>
             </intent-filter>
         </service>
+        <service android:name="android.support.v4.media.StubRemoteMediaBrowserServiceCompat"
+                 android:process=":remote">
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowserService"/>
+            </intent-filter>
+        </service>
     </application>
 
     <instrumentation
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index fd87a76..ad3383a 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -19,7 +19,9 @@
 import static android.support.test.InstrumentationRegistry.getInstrumentation;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.fail;
 
@@ -58,6 +60,10 @@
     private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
             "android.support.mediacompat.test",
             "android.support.v4.media.StubMediaBrowserServiceCompat");
+    private static final ComponentName TEST_REMOTE_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.test",
+            "android.support.v4.media.StubRemoteMediaBrowserServiceCompat");
+
     private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
             "invalid.package", "invalid.ServiceClassName");
     private final StubConnectionCallback mConnectionCallback = new StubConnectionCallback();
@@ -71,10 +77,10 @@
     public void testMediaBrowser() {
         resetCallbacks();
         createMediaBrowser(TEST_BROWSER_SERVICE);
-        assertEquals(false, mMediaBrowser.isConnected());
+        assertFalse(mMediaBrowser.isConnected());
 
         connectMediaBrowserService();
-        assertEquals(true, mMediaBrowser.isConnected());
+        assertTrue(mMediaBrowser.isConnected());
 
         assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
         assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
@@ -90,6 +96,33 @@
                 return !mMediaBrowser.isConnected();
             }
         }.run();
+        assertFalse(mMediaBrowser.isConnected());
+    }
+
+    @Test
+    @SmallTest
+    public void testMediaBrowserWithRemoteService() {
+        resetCallbacks();
+        createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
+        assertFalse(mMediaBrowser.isConnected());
+
+        connectMediaBrowserService();
+        assertTrue(mMediaBrowser.isConnected());
+
+        assertEquals(TEST_REMOTE_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+        assertEquals(StubRemoteMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+        assertEquals(StubRemoteMediaBrowserServiceCompat.EXTRAS_VALUE,
+                mMediaBrowser.getExtras().getString(
+                        StubRemoteMediaBrowserServiceCompat.EXTRAS_KEY));
+
+        mMediaBrowser.disconnect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return !mMediaBrowser.isConnected();
+            }
+        }.run();
+        assertFalse(mMediaBrowser.isConnected());
     }
 
     @Test
diff --git a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
new file mode 100644
index 0000000..9d7e73d
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+public class StubRemoteMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
+    static final String EXTRAS_KEY = "test_extras_key";
+    static final String EXTRAS_VALUE = "test_extras_value";
+
+    static final String MEDIA_ID_ROOT = "test_media_id_root";
+
+    static final String[] MEDIA_ID_CHILDREN = new String[]{
+            "test_media_id_children_0", "test_media_id_children_1",
+            "test_media_id_children_2", "test_media_id_children_3"
+    };
+
+    private static MediaSessionCompat mSession;
+    private Bundle mExtras;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mSession = new MediaSessionCompat(this, "StubRemoteMediaBrowserServiceCompat");
+        setSessionToken(mSession.getSessionToken());
+    }
+
+    @Override
+    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+        mExtras = new Bundle();
+        mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+        return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
+    }
+
+    @Override
+    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+        List<MediaItem> mediaItems = new ArrayList<>();
+        if (MEDIA_ID_ROOT.equals(parentMediaId)) {
+            Bundle rootHints = getBrowserRootHints();
+            for (String id : MEDIA_ID_CHILDREN) {
+                mediaItems.add(createMediaItem(id));
+            }
+            result.sendResult(mediaItems);
+        }
+    }
+
+    private MediaItem createMediaItem(String id) {
+        return new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId(id).setExtras(getBrowserRootHints()).build(),
+                MediaItem.FLAG_BROWSABLE);
+    }
+}
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java b/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
index 1fe0874..2d0ab62 100644
--- a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
+++ b/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
@@ -55,10 +55,18 @@
         return window.getSharedElementEnterTransition();
     }
 
+    public static void setSharedElementEnterTransition(Window window, Object transition) {
+        window.setSharedElementEnterTransition((Transition) transition);
+    }
+
     public static Object getSharedElementReturnTransition(Window window) {
         return window.getSharedElementReturnTransition();
     }
 
+    public static void setSharedElementReturnTransition(Window window, Object transition) {
+        window.setSharedElementReturnTransition((Transition) transition);
+    }
+
     public static Object getSharedElementExitTransition(Window window) {
         return window.getSharedElementExitTransition();
     }
@@ -71,10 +79,18 @@
         return window.getEnterTransition();
     }
 
+    public static void setEnterTransition(Window window, Object transition) {
+        window.setEnterTransition((Transition) transition);
+    }
+
     public static Object getReturnTransition(Window window) {
         return window.getReturnTransition();
     }
 
+    public static void setReturnTransition(Window window, Object transition) {
+        window.setReturnTransition((Transition) transition);
+    }
+
     public static Object getExitTransition(Window window) {
         return window.getExitTransition();
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
index ff5ef2e..b123b9c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
@@ -13,12 +13,12 @@
  */
 package android.support.v17.leanback.app;
 
-import static android.support.v17.leanback.util.StateMachine.STATUS_EXECUTED;
-
 import android.os.Bundle;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
 import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.util.StateMachine.Condition;
+import android.support.v17.leanback.util.StateMachine.Event;
 import android.support.v17.leanback.util.StateMachine.State;
 import android.view.View;
 import android.view.ViewTreeObserver;
@@ -30,15 +30,20 @@
 class BaseFragment extends BrandedFragment {
 
     /**
-     * Condition: {@link TransitionHelper#systemSupportsEntranceTransitions()} is true
-     * Action: none
+     * The start state for all
      */
-    private final State STATE_ALLOWED = new State() {
-        @Override
-        public boolean canRun() {
-            return TransitionHelper.systemSupportsEntranceTransitions();
-        }
+    final State STATE_START = new State("START", true, false);
 
+    /**
+     * Initial State for ENTRNACE transition.
+     */
+    final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
+
+    /**
+     * prepareEntranceTransition is just called, but view not ready yet. We can enable the
+     * busy spinner.
+     */
+    final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
         @Override
         public void run() {
             mProgressBarManager.show();
@@ -46,15 +51,13 @@
     };
 
     /**
-     * Condition: {@link #isReadyForPrepareEntranceTransition()} is true
-     * Action: {@link #onEntranceTransitionPrepare()} }
+     * prepareEntranceTransition is called and main content view to slide in was created, so we can
+     * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
+     * in this State, the process is very different in subclass, e.g. BrowseFragment hide header
+     * views and hide main fragment view in two steps.
      */
-    private final State STATE_PREPARE = new State() {
-        @Override
-        public boolean canRun() {
-            return isReadyForPrepareEntranceTransition();
-        }
-
+    final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
+            "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
         @Override
         public void run() {
             onEntranceTransitionPrepare();
@@ -62,15 +65,9 @@
     };
 
     /**
-     * Condition: {@link #isReadyForStartEntranceTransition()} is true
-     * Action: {@link #onExecuteEntranceTransition()} }
+     * execute the entrance transition.
      */
-    private final State STATE_START = new State() {
-        @Override
-        public boolean canRun() {
-            return isReadyForStartEntranceTransition();
-        }
-
+    final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
         @Override
         public void run() {
             mProgressBarManager.hide();
@@ -78,26 +75,109 @@
         }
     };
 
-    final StateMachine mEnterTransitionStates;
+    /**
+     * execute onEntranceTransitionEnd.
+     */
+    final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
+        @Override
+        public void run() {
+            onEntranceTransitionEnd();
+        }
+    };
+
+    /**
+     * either entrance transition completed or skipped
+     */
+    final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
+
+    /**
+     * Event fragment.onCreate()
+     */
+    final Event EVT_ON_CREATE = new Event("onCreate");
+
+    /**
+     * Event fragment.onViewCreated()
+     */
+    final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
+
+    /**
+     * Event for {@link #prepareEntranceTransition()} is called.
+     */
+    final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
+
+    /**
+     * Event for {@link #startEntranceTransition()} is called.
+     */
+    final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
+
+    /**
+     * Event for entrance transition is ended through Transition listener.
+     */
+    final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
+
+    /**
+     * Event for skipping entrance transition if not supported.
+     */
+    final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
+        @Override
+        public boolean canProceed() {
+            return !TransitionHelper.systemSupportsEntranceTransitions();
+        }
+    };
+
+    final StateMachine mStateMachine = new StateMachine();
 
     Object mEntranceTransition;
     final ProgressBarManager mProgressBarManager = new ProgressBarManager();
 
     BaseFragment() {
-        mEnterTransitionStates = new StateMachine();
-        mEnterTransitionStates.addState(STATE_ALLOWED);
-        mEnterTransitionStates.addState(STATE_PREPARE);
-        mEnterTransitionStates.addState(STATE_START);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        createStateMachineStates();
+        createStateMachineTransitions();
+        mStateMachine.start();
+        super.onCreate(savedInstanceState);
+        mStateMachine.fireEvent(EVT_ON_CREATE);
+    }
+
+    void createStateMachineStates() {
+        mStateMachine.addState(STATE_START);
+        mStateMachine.addState(STATE_ENTRANCE_INIT);
+        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
+        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
+        mStateMachine.addState(STATE_ENTRANCE_PERFORM);
+        mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
+        mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
+    }
+
+    void createStateMachineTransitions() {
+        mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+                COND_TRANSITION_NOT_SUPPORTED);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+                EVT_ON_CREATEVIEW);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
+                EVT_PREPARE_ENTRANCE);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                EVT_ON_CREATEVIEW);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_PERFORM,
+                EVT_START_ENTRANCE);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                STATE_ENTRANCE_PERFORM);
+        mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
+                STATE_ENTRANCE_ON_ENDED,
+                EVT_ENTRANCE_END);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        performPendingStates();
-    }
-
-    final void performPendingStates() {
-        mEnterTransitionStates.runPendingStates();
+        mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
     }
 
     /**
@@ -127,18 +207,7 @@
      * override the default transition that browse and details provides.
      */
     public void prepareEntranceTransition() {
-        mEnterTransitionStates.runState(STATE_ALLOWED);
-        mEnterTransitionStates.runState(STATE_PREPARE);
-    }
-
-    /**
-     * Return true if entrance transition is enabled and not started yet.
-     * Entrance transition can only be executed once and isEntranceTransitionEnabled()
-     * is reset to false after entrance transition is started.
-     */
-    boolean isEntranceTransitionEnabled() {
-        // Enabled when passed STATE_ALLOWED in prepareEntranceTransition call.
-        return STATE_ALLOWED.getStatus() == STATUS_EXECUTED;
+        mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
     }
 
     /**
@@ -179,26 +248,6 @@
     }
 
     /**
-     * Returns true if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     * @return True if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     */
-    boolean isReadyForPrepareEntranceTransition() {
-        return getView() != null;
-    }
-
-    /**
-     * Returns true if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     * @return True if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     */
-    boolean isReadyForStartEntranceTransition() {
-        return getView() != null;
-    }
-
-    /**
      * When fragment finishes loading data, it should call startEntranceTransition()
      * to execute the entrance transition.
      * startEntranceTransition() will start transition only if both two conditions
@@ -210,7 +259,7 @@
      * and executed when view is created.
      */
     public void startEntranceTransition() {
-        mEnterTransitionStates.runState(STATE_START);
+        mStateMachine.fireEvent(EVT_START_ENTRANCE);
     }
 
     void onExecuteEntranceTransition() {
@@ -225,9 +274,11 @@
                     return true;
                 }
                 internalCreateEntranceTransition();
+                onEntranceTransitionStart();
                 if (mEntranceTransition != null) {
-                    onEntranceTransitionStart();
                     runEntranceTransition(mEntranceTransition);
+                } else {
+                    mStateMachine.fireEvent(EVT_ENTRANCE_END);
                 }
                 return false;
             }
@@ -244,8 +295,7 @@
             @Override
             public void onTransitionEnd(Object transition) {
                 mEntranceTransition = null;
-                onEntranceTransitionEnd();
-                mEnterTransitionStates.resetStatus();
+                mStateMachine.fireEvent(EVT_ENTRANCE_END);
             }
         });
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
index 62ee0d4..0db81b3 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
@@ -16,12 +16,12 @@
  */
 package android.support.v17.leanback.app;
 
-import static android.support.v17.leanback.util.StateMachine.STATUS_EXECUTED;
-
 import android.os.Bundle;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
 import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.util.StateMachine.Condition;
+import android.support.v17.leanback.util.StateMachine.Event;
 import android.support.v17.leanback.util.StateMachine.State;
 import android.view.View;
 import android.view.ViewTreeObserver;
@@ -33,15 +33,20 @@
 class BaseSupportFragment extends BrandedSupportFragment {
 
     /**
-     * Condition: {@link TransitionHelper#systemSupportsEntranceTransitions()} is true
-     * Action: none
+     * The start state for all
      */
-    private final State STATE_ALLOWED = new State() {
-        @Override
-        public boolean canRun() {
-            return TransitionHelper.systemSupportsEntranceTransitions();
-        }
+    final State STATE_START = new State("START", true, false);
 
+    /**
+     * Initial State for ENTRNACE transition.
+     */
+    final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
+
+    /**
+     * prepareEntranceTransition is just called, but view not ready yet. We can enable the
+     * busy spinner.
+     */
+    final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
         @Override
         public void run() {
             mProgressBarManager.show();
@@ -49,15 +54,13 @@
     };
 
     /**
-     * Condition: {@link #isReadyForPrepareEntranceTransition()} is true
-     * Action: {@link #onEntranceTransitionPrepare()} }
+     * prepareEntranceTransition is called and main content view to slide in was created, so we can
+     * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
+     * in this State, the process is very different in subclass, e.g. BrowseSupportFragment hide header
+     * views and hide main fragment view in two steps.
      */
-    private final State STATE_PREPARE = new State() {
-        @Override
-        public boolean canRun() {
-            return isReadyForPrepareEntranceTransition();
-        }
-
+    final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
+            "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
         @Override
         public void run() {
             onEntranceTransitionPrepare();
@@ -65,15 +68,9 @@
     };
 
     /**
-     * Condition: {@link #isReadyForStartEntranceTransition()} is true
-     * Action: {@link #onExecuteEntranceTransition()} }
+     * execute the entrance transition.
      */
-    private final State STATE_START = new State() {
-        @Override
-        public boolean canRun() {
-            return isReadyForStartEntranceTransition();
-        }
-
+    final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
         @Override
         public void run() {
             mProgressBarManager.hide();
@@ -81,26 +78,109 @@
         }
     };
 
-    final StateMachine mEnterTransitionStates;
+    /**
+     * execute onEntranceTransitionEnd.
+     */
+    final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
+        @Override
+        public void run() {
+            onEntranceTransitionEnd();
+        }
+    };
+
+    /**
+     * either entrance transition completed or skipped
+     */
+    final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
+
+    /**
+     * Event fragment.onCreate()
+     */
+    final Event EVT_ON_CREATE = new Event("onCreate");
+
+    /**
+     * Event fragment.onViewCreated()
+     */
+    final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
+
+    /**
+     * Event for {@link #prepareEntranceTransition()} is called.
+     */
+    final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
+
+    /**
+     * Event for {@link #startEntranceTransition()} is called.
+     */
+    final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
+
+    /**
+     * Event for entrance transition is ended through Transition listener.
+     */
+    final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
+
+    /**
+     * Event for skipping entrance transition if not supported.
+     */
+    final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
+        @Override
+        public boolean canProceed() {
+            return !TransitionHelper.systemSupportsEntranceTransitions();
+        }
+    };
+
+    final StateMachine mStateMachine = new StateMachine();
 
     Object mEntranceTransition;
     final ProgressBarManager mProgressBarManager = new ProgressBarManager();
 
     BaseSupportFragment() {
-        mEnterTransitionStates = new StateMachine();
-        mEnterTransitionStates.addState(STATE_ALLOWED);
-        mEnterTransitionStates.addState(STATE_PREPARE);
-        mEnterTransitionStates.addState(STATE_START);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        createStateMachineStates();
+        createStateMachineTransitions();
+        mStateMachine.start();
+        super.onCreate(savedInstanceState);
+        mStateMachine.fireEvent(EVT_ON_CREATE);
+    }
+
+    void createStateMachineStates() {
+        mStateMachine.addState(STATE_START);
+        mStateMachine.addState(STATE_ENTRANCE_INIT);
+        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
+        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
+        mStateMachine.addState(STATE_ENTRANCE_PERFORM);
+        mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
+        mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
+    }
+
+    void createStateMachineTransitions() {
+        mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+                COND_TRANSITION_NOT_SUPPORTED);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+                EVT_ON_CREATEVIEW);
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
+                EVT_PREPARE_ENTRANCE);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                EVT_ON_CREATEVIEW);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_PERFORM,
+                EVT_START_ENTRANCE);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                STATE_ENTRANCE_PERFORM);
+        mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
+                STATE_ENTRANCE_ON_ENDED,
+                EVT_ENTRANCE_END);
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        performPendingStates();
-    }
-
-    final void performPendingStates() {
-        mEnterTransitionStates.runPendingStates();
+        mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
     }
 
     /**
@@ -130,18 +210,7 @@
      * override the default transition that browse and details provides.
      */
     public void prepareEntranceTransition() {
-        mEnterTransitionStates.runState(STATE_ALLOWED);
-        mEnterTransitionStates.runState(STATE_PREPARE);
-    }
-
-    /**
-     * Return true if entrance transition is enabled and not started yet.
-     * Entrance transition can only be executed once and isEntranceTransitionEnabled()
-     * is reset to false after entrance transition is started.
-     */
-    boolean isEntranceTransitionEnabled() {
-        // Enabled when passed STATE_ALLOWED in prepareEntranceTransition call.
-        return STATE_ALLOWED.getStatus() == STATUS_EXECUTED;
+        mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
     }
 
     /**
@@ -182,26 +251,6 @@
     }
 
     /**
-     * Returns true if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     * @return True if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     */
-    boolean isReadyForPrepareEntranceTransition() {
-        return getView() != null;
-    }
-
-    /**
-     * Returns true if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     * @return True if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
-     * Subclass may override and add additional conditions.
-     */
-    boolean isReadyForStartEntranceTransition() {
-        return getView() != null;
-    }
-
-    /**
      * When fragment finishes loading data, it should call startEntranceTransition()
      * to execute the entrance transition.
      * startEntranceTransition() will start transition only if both two conditions
@@ -213,7 +262,7 @@
      * and executed when view is created.
      */
     public void startEntranceTransition() {
-        mEnterTransitionStates.runState(STATE_START);
+        mStateMachine.fireEvent(EVT_START_ENTRANCE);
     }
 
     void onExecuteEntranceTransition() {
@@ -228,9 +277,11 @@
                     return true;
                 }
                 internalCreateEntranceTransition();
+                onEntranceTransitionStart();
                 if (mEntranceTransition != null) {
-                    onEntranceTransitionStart();
                     runEntranceTransition(mEntranceTransition);
+                } else {
+                    mStateMachine.fireEvent(EVT_ENTRANCE_END);
                 }
                 return false;
             }
@@ -247,8 +298,7 @@
             @Override
             public void onTransitionEnd(Object transition) {
                 mEntranceTransition = null;
-                onEntranceTransitionEnd();
-                mEnterTransitionStates.resetStatus();
+                mStateMachine.fireEvent(EVT_ENTRANCE_END);
             }
         });
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
index f16d569..35350e4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
@@ -150,6 +150,7 @@
         }
         if (mTitleView != null && view instanceof ViewGroup) {
             mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
+            mTitleHelper.showTitle(mShowingTitle);
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
index 1a0d81f..9c42780 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
@@ -153,6 +153,7 @@
         }
         if (mTitleView != null && view instanceof ViewGroup) {
             mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
+            mTitleHelper.showTitle(mShowingTitle);
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index 8bbe601..c64673b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -28,6 +28,8 @@
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
 import android.support.v17.leanback.widget.InvisibleRowPresenter;
 import android.support.v17.leanback.widget.ListRow;
@@ -86,6 +88,56 @@
     private static final String IS_PAGE_ROW = "isPageRow";
     private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
 
+    /**
+     * State to hide headers fragment.
+     */
+    final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            setEntranceTransitionStartState();
+        }
+    };
+
+    /**
+     * Event for Header fragment view is created, we could perform
+     * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+     */
+    final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+    /**
+     * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+     * {@link #onEntranceTransitionPrepare()}.
+     */
+    final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+    /**
+     * Event that data for the screen is ready, this is additional requirement to launch entrance
+     * transition.
+     */
+    final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        // when headers fragment view is created we could setEntranceTransitionStartState()
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+                EVT_HEADER_VIEW_CREATED);
+
+        // add additional requirement for onEntranceTransitionPrepare()
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                EVT_MAIN_FRAGMENT_VIEW_CREATED);
+        // add additional requirement to launch entrance transition.
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,  STATE_ENTRANCE_PERFORM,
+                EVT_SCREEN_DATA_READY);
+    }
+
     final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
         int mLastEntryCount;
         int mIndexOfHeadersBackStack;
@@ -252,20 +304,21 @@
      */
     private final class FragmentHostImpl implements FragmentHost {
         boolean mShowTitleView = true;
-        boolean mDataReady = false;
 
         FragmentHostImpl() {
         }
 
         @Override
         public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
-            performPendingStates();
+            mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+            if (!mIsPageRow) {
+                // If it's not a PageRow: it's a ListRow, so we already have data ready.
+                mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+            }
         }
 
         @Override
         public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
-            mDataReady = true;
-
             // If fragment host is not the currently active fragment (in BrowseFragment), then
             // ignore the request.
             if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
@@ -277,7 +330,7 @@
                 return;
             }
 
-            performPendingStates();
+            mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
         }
 
         @Override
@@ -1224,17 +1277,6 @@
         }
     }
 
-    @Override
-    boolean isReadyForPrepareEntranceTransition() {
-        return mMainFragment != null && mMainFragment.getView() != null;
-    }
-
-    @Override
-    boolean isReadyForStartEntranceTransition() {
-        return mMainFragment != null && mMainFragment.getView() != null
-                && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady);
-    }
-
     void createHeadersTransition() {
         mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(this),
                 mShowingHeaders
@@ -1455,7 +1497,6 @@
             swapToMainFragment();
             expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
             setupMainFragment();
-            performPendingStates();
         }
     }
 
@@ -1565,9 +1606,7 @@
             showHeaders(mShowingHeaders);
         }
 
-        if (isEntranceTransitionEnabled()) {
-            setEntranceTransitionStartState();
-        }
+        mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
     }
 
     private void onExpandTransitionStart(boolean expand, final Runnable callback) {
@@ -1683,8 +1722,6 @@
     @Override
     protected void onEntranceTransitionPrepare() {
         mHeadersFragment.onTransitionPrepare();
-        // setEntranceTransitionStartState() might be called when mMainFragment is null,
-        // make sure it is called.
         mMainFragmentAdapter.setEntranceTransitionState(false);
         mMainFragmentAdapter.onTransitionPrepare();
     }
@@ -1718,7 +1755,9 @@
     void setEntranceTransitionStartState() {
         setHeadersOnScreen(false);
         setSearchOrbViewOnScreen(false);
-        mMainFragmentAdapter.setEntranceTransitionState(false);
+        // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+        // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+        // one when setEntranceTransitionStartState() is called.
     }
 
     void setEntranceTransitionEndState() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
index 9215d09..967bf56 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -31,6 +31,8 @@
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
 import android.support.v17.leanback.widget.InvisibleRowPresenter;
 import android.support.v17.leanback.widget.ListRow;
@@ -89,6 +91,56 @@
     private static final String IS_PAGE_ROW = "isPageRow";
     private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
 
+    /**
+     * State to hide headers fragment.
+     */
+    final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            setEntranceTransitionStartState();
+        }
+    };
+
+    /**
+     * Event for Header fragment view is created, we could perform
+     * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+     */
+    final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+    /**
+     * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+     * {@link #onEntranceTransitionPrepare()}.
+     */
+    final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+    /**
+     * Event that data for the screen is ready, this is additional requirement to launch entrance
+     * transition.
+     */
+    final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        // when headers fragment view is created we could setEntranceTransitionStartState()
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+                EVT_HEADER_VIEW_CREATED);
+
+        // add additional requirement for onEntranceTransitionPrepare()
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+                EVT_MAIN_FRAGMENT_VIEW_CREATED);
+        // add additional requirement to launch entrance transition.
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,  STATE_ENTRANCE_PERFORM,
+                EVT_SCREEN_DATA_READY);
+    }
+
     final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
         int mLastEntryCount;
         int mIndexOfHeadersBackStack;
@@ -255,20 +307,21 @@
      */
     private final class FragmentHostImpl implements FragmentHost {
         boolean mShowTitleView = true;
-        boolean mDataReady = false;
 
         FragmentHostImpl() {
         }
 
         @Override
         public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
-            performPendingStates();
+            mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+            if (!mIsPageRow) {
+                // If it's not a PageRow: it's a ListRow, so we already have data ready.
+                mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+            }
         }
 
         @Override
         public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
-            mDataReady = true;
-
             // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
             // ignore the request.
             if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
@@ -280,7 +333,7 @@
                 return;
             }
 
-            performPendingStates();
+            mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
         }
 
         @Override
@@ -1227,17 +1280,6 @@
         }
     }
 
-    @Override
-    boolean isReadyForPrepareEntranceTransition() {
-        return mMainFragment != null && mMainFragment.getView() != null;
-    }
-
-    @Override
-    boolean isReadyForStartEntranceTransition() {
-        return mMainFragment != null && mMainFragment.getView() != null
-                && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady);
-    }
-
     void createHeadersTransition() {
         mHeadersTransition = TransitionHelper.loadTransition(getContext(),
                 mShowingHeaders
@@ -1458,7 +1500,6 @@
             swapToMainFragment();
             expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
             setupMainFragment();
-            performPendingStates();
         }
     }
 
@@ -1568,9 +1609,7 @@
             showHeaders(mShowingHeaders);
         }
 
-        if (isEntranceTransitionEnabled()) {
-            setEntranceTransitionStartState();
-        }
+        mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
     }
 
     private void onExpandTransitionStart(boolean expand, final Runnable callback) {
@@ -1686,8 +1725,6 @@
     @Override
     protected void onEntranceTransitionPrepare() {
         mHeadersSupportFragment.onTransitionPrepare();
-        // setEntranceTransitionStartState() might be called when mMainFragment is null,
-        // make sure it is called.
         mMainFragmentAdapter.setEntranceTransitionState(false);
         mMainFragmentAdapter.onTransitionPrepare();
     }
@@ -1721,7 +1758,9 @@
     void setEntranceTransitionStartState() {
         setHeadersOnScreen(false);
         setSearchOrbViewOnScreen(false);
-        mMainFragmentAdapter.setEntranceTransitionState(false);
+        // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+        // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+        // one when setEntranceTransitionStartState() is called.
     }
 
     void setEntranceTransitionEndState() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
index c372888..5bae3d0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -77,6 +77,7 @@
         this.mDetailsParallax = detailsParallax;
         this.mBackgroundDrawable = backgroundDrawable;
         mBackgroundDrawableVisible = true;
+        mBackgroundDrawable.setAlpha(255);
         startParallax();
     }
 
@@ -163,9 +164,23 @@
         }, CROSSFADE_DELAY);
     }
 
-    private void crossFadeBackgroundToVideo(final boolean crossFadeToVideo) {
+    void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {
+        crossFadeBackgroundToVideo(crossFadeToVideo, false);
+    }
+
+    void crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate) {
         final boolean newVisible = !crossFadeToVideo;
         if (mBackgroundDrawableVisible == newVisible) {
+            if (immediate) {
+                if (mBackgroundAnimator != null) {
+                    mBackgroundAnimator.cancel();
+                    mBackgroundAnimator = null;
+                }
+                if (mBackgroundDrawable != null) {
+                    mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
+                    return;
+                }
+            }
             return;
         }
         mBackgroundDrawableVisible = newVisible;
@@ -180,6 +195,10 @@
         if (mBackgroundDrawable == null) {
             return;
         }
+        if (immediate) {
+            mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
+            return;
+        }
         mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
         mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
         mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 7e54146..57a85a0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -24,6 +24,8 @@
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
 import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -41,6 +43,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.Window;
 
 import java.lang.ref.WeakReference;
 
@@ -87,22 +90,190 @@
     static final String TAG = "DetailsFragment";
     static boolean DEBUG = false;
 
+    final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            mRowsFragment.setEntranceTransitionState(false);
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
+
+    void switchToVideoBeforeVideoFragmentCreated() {
+        // if the video fragment is not ready: immediately fade out covering drawable,
+        // hide title and mark mPendingFocusOnVideo and set focus on it later.
+        mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+        showTitle(false);
+        mPendingFocusOnVideo = true;
+        slideOutGridView();
+    }
+
+    final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
+            false, false) {
+        @Override
+        public void run() {
+            switchToVideoBeforeVideoFragmentCreated();
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
+            false, false) {
+        @Override
+        public void run() {
+            if (mWaitEnterTransitionTimeout != null) {
+                mWaitEnterTransitionTimeout.mRef.clear();
+            }
+            // clear the activity enter/sharedElement transition, return transitions are kept.
+            // keep the return transitions and clear enter transition
+            if (getActivity() != null) {
+                Window window = getActivity().getWindow();
+                Object returnTransition = TransitionHelper.getReturnTransition(window);
+                Object sharedReturnTransition = TransitionHelper
+                        .getSharedElementReturnTransition(window);
+                TransitionHelper.setEnterTransition(window, null);
+                TransitionHelper.setSharedElementEnterTransition(window, null);
+                TransitionHelper.setReturnTransition(window, returnTransition);
+                TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
+            }
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
+            true, false);
+
+    final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
+        @Override
+        public void run() {
+            Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
+            TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
+        @Override
+        public void run() {
+            if (mWaitEnterTransitionTimeout == null) {
+                new WaitEnterTransitionTimeout(DetailsFragment.this);
+            }
+        }
+    };
+
     /**
-     * Flag for "possibly" having enter transition not finished yet.
-     * @see #mStartAndTransitionFlag
+     * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+     * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
      */
-    static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
-    /**
-     * Flag for having entrance transition not finished yet.
-     * @see #mStartAndTransitionFlag
-     */
-    static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
-    /**
-     * Flag that onStart() has been called and about to call onSafeStart() when
-     * pending transitions are finished.
-     * @see #mStartAndTransitionFlag
-     */
-    static final int PF_PENDING_START = 0x1 << 2;
+    static class WaitEnterTransitionTimeout implements Runnable {
+        static final long WAIT_ENTERTRANSITION_START = 200;
+
+        final WeakReference<DetailsFragment> mRef;
+
+        WaitEnterTransitionTimeout(DetailsFragment f) {
+            mRef = new WeakReference(f);
+            f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+        }
+
+        @Override
+        public void run() {
+            DetailsFragment f = mRef.get();
+            if (f != null) {
+                f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
+            }
+        }
+    }
+
+    final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
+        @Override
+        public void run() {
+            onSafeStart();
+        }
+    };
+
+    final Event EVT_ONSTART = new Event("onStart");
+
+    final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
+
+    final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
+
+    final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
+
+    final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+        mStateMachine.addState(STATE_ON_SAFE_START);
+        mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        /**
+         * Part 1: Processing enter transitions after fragment.onCreate
+         */
+        mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
+        // if transition is not supported, skip to complete
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+                COND_TRANSITION_NOT_SUPPORTED);
+        // if transition is not set on Activity, skip to complete
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+                EVT_NO_ENTER_TRANSITION);
+        // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
+        // complete.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
+                EVT_SWITCH_TO_VIDEO);
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
+        // once after onCreateView, we cannot skip the enter transition, add a listener and wait
+        // it to finish
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
+                EVT_ON_CREATEVIEW);
+        // when enter transition finishes, go to complete, however this might never happen if
+        // the activity is not giving transition options in startActivity, there is no API to query
+        // if this activity is started in a enter transition mode. So we rely on a timer below:
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+                STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
+        // we are expecting app to start delayed enter transition shortly after details row is
+        // loaded, so create a timer and wait for enter transition start.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+                STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
+        // if enter transition not started in the timer, skip to DONE, this can be also true when
+        // startActivity is not giving transition option.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
+                EVT_ENTER_TRANSIITON_DONE);
+
+        /**
+         * Part 2: modification to the entrance transition defined in BaseFragment
+         */
+        // Must finish enter transition before perform entrance transition.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
+        // Calling switch to video would hide immediately and skip entrance transition
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+                EVT_SWITCH_TO_VIDEO);
+        mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
+        // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
+        // still need to do the switchToVideo.
+        mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+                EVT_SWITCH_TO_VIDEO);
+
+        // for once the view is created in onStart and prepareEntranceTransition was called, we
+        // could setEntranceStartState:
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
+
+        /**
+         * Part 3: onSafeStart()
+         */
+        // for onSafeStart: the condition is onStart called, entrance transition complete
+        mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
+        mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
+    }
 
     private class SetSelectionRunnable implements Runnable {
         int mPosition;
@@ -120,33 +291,6 @@
         }
     }
 
-    /**
-     * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
-     * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
-     * @see #mStartAndTransitionFlag
-     */
-    static class WaitEnterTransitionTimeout implements Runnable {
-        static final long WAIT_ENTERTRANSITION_START = 200;
-
-        final WeakReference<DetailsFragment> mRef;
-
-        WaitEnterTransitionTimeout(DetailsFragment f) {
-            mRef = new WeakReference(f);
-            f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
-        }
-
-        @Override
-        public void run() {
-            DetailsFragment f = mRef.get();
-            if (f != null) {
-                f.clearPendingEnterTransition();
-            }
-        }
-    }
-
-    /**
-     * @see #mStartAndTransitionFlag
-     */
     TransitionListener mEnterTransitionListener = new TransitionListener() {
         @Override
         public void onTransitionStart(Object transition) {
@@ -159,12 +303,12 @@
 
         @Override
         public void onTransitionCancel(Object transition) {
-            clearPendingEnterTransition();
+            mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
         }
 
         @Override
         public void onTransitionEnd(Object transition) {
-            clearPendingEnterTransition();
+            mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
         }
     };
 
@@ -187,23 +331,10 @@
     BaseOnItemViewClickedListener mOnItemViewClickedListener;
     DetailsFragmentBackgroundController mDetailsBackgroundController;
 
+    // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
+    // true, we will focus to VideoFragment immediately after video fragment's view is created.
+    boolean mPendingFocusOnVideo = false;
 
-    /**
-     * Flags for enter transition, entrance transition and onStart.  When onStart() is called
-     * and both enter transiton and entrance transition are finished, we could call onSafeStart().
-     * 1. in onCreate:
-     *      if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
-     *      if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
-     *      user will run enterTransition or not.
-     * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
-     * start. If enter transition onTransitionStart is not invoked with a period, we can assume
-     * there is no enter transition running, then WaitEnterTransitionTimeout will clear
-     * PF_ENTER_TRANSITION_PENDING.
-     * 3. When enterTransition runs (either postponed or not),  we will stop the
-     * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
-     * PF_ENTER_TRANSITION_PENDING.
-     */
-    int mStartAndTransitionFlag = 0;
     WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
 
     Object mSceneAfterEntranceTransition;
@@ -287,14 +418,15 @@
         Activity activity = getActivity();
         if (activity != null) {
             Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
-            if (transition != null) {
-                mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
-                TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+            if (transition == null) {
+                mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
             }
             transition = TransitionHelper.getReturnTransition(activity.getWindow());
             if (transition != null) {
                 TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
             }
+        } else {
+            mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
         }
     }
 
@@ -449,6 +581,22 @@
         }
     }
 
+    void switchToVideo() {
+        if (mVideoFragment != null && mVideoFragment.getView() != null) {
+            mVideoFragment.getView().requestFocus();
+        } else {
+            mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
+        }
+    }
+
+    void switchToRows() {
+        mPendingFocusOnVideo = false;
+        VerticalGridView verticalGridView = getVerticalGridView();
+        if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
+            verticalGridView.requestFocus();
+        }
+    }
+
     /**
      * This method asks DetailsFragmentBackgroundController to add a fragment for rendering video.
      * In case the fragment is already there, it will return the existing one. The method must be
@@ -465,6 +613,18 @@
             ft2.add(android.support.v17.leanback.R.id.video_surface_container,
                     fragment = mDetailsBackgroundController.onCreateVideoFragment());
             ft2.commit();
+            if (mPendingFocusOnVideo) {
+                // wait next cycle for Fragment view created so we can focus on it.
+                // This is a bit hack eventually we will do commitNow() which get view immediately.
+                getView().post(new Runnable() {
+                    public void run() {
+                        if (getView() != null) {
+                            switchToVideo();
+                        }
+                        mPendingFocusOnVideo = false;
+                    }
+                });
+            }
         }
         mVideoFragment = fragment;
         return mVideoFragment;
@@ -473,7 +633,7 @@
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
         if (( mRowsFragment != null && mRowsFragment.getView() != null
-                && mRowsFragment.getView().hasFocus())
+                && mRowsFragment.getView().hasFocus() && !mPendingFocusOnVideo)
                 && (adapter == null || adapter.size() == 0
                 || (getVerticalGridView().getSelectedPosition() == 0
                 && getVerticalGridView().getSelectedSubPosition() == 0))) {
@@ -484,10 +644,8 @@
         if (adapter != null && adapter.size() > selectedPosition) {
             final VerticalGridView gridView = getVerticalGridView();
             final int count = gridView.getChildCount();
-            if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
-                if (mWaitEnterTransitionTimeout == null) {
-                    mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
-                }
+            if (count > 0) {
+                mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
             }
             for (int i = 0; i < count; i++) {
                 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
@@ -501,25 +659,6 @@
         }
     }
 
-    void clearPendingEnterTransition() {
-        if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
-            mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
-            dispatchOnStartAndTransitionFinished();
-        }
-    }
-
-    void dispatchOnStartAndTransitionFinished() {
-        /**
-         * if onStart() was called and there is no pending enter transition or entrance transition.
-         */
-        if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
-                && (mStartAndTransitionFlag
-                & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
-            mStartAndTransitionFlag &= ~PF_PENDING_START;
-            onSafeStart();
-        }
-    }
-
     /**
      * Called when onStart and enter transition (postponed/none postponed) and entrance transition
      * are all finished.
@@ -614,17 +753,14 @@
     public void onStart() {
         super.onStart();
 
-        mStartAndTransitionFlag |= PF_PENDING_START;
-        dispatchOnStartAndTransitionFinished();
-
         setupChildFragmentLayout();
-        if (isEntranceTransitionEnabled()) {
-            mRowsFragment.setEntranceTransitionState(false);
-        }
+        mStateMachine.fireEvent(EVT_ONSTART);
         if (mDetailsParallax != null) {
             mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
         }
-        if (!getView().hasFocus()) {
+        if (mPendingFocusOnVideo) {
+            slideOutGridView();
+        } else if (!getView().hasFocus()) {
             mRowsFragment.getVerticalGridView().requestFocus();
         }
     }
@@ -642,14 +778,11 @@
 
     @Override
     protected void onEntranceTransitionEnd() {
-        mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
-        dispatchOnStartAndTransitionFinished();
         mRowsFragment.onTransitionEnd();
     }
 
     @Override
     protected void onEntranceTransitionPrepare() {
-        mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
         mRowsFragment.onTransitionPrepare();
     }
 
@@ -711,8 +844,10 @@
             public void onRequestChildFocus(View child, View focused) {
                 if (child != mRootView.getFocusedChild()) {
                     if (child.getId() == R.id.details_fragment_root) {
-                        slideInGridView();
-                        showTitle(true);
+                        if (!mPendingFocusOnVideo) {
+                            slideInGridView();
+                            showTitle(true);
+                        }
                     } else if (child.getId() == R.id.video_surface_container) {
                         slideOutGridView();
                         showTitle(false);
@@ -758,8 +893,10 @@
                 if (mVideoFragment != null && mVideoFragment.getView() != null
                         && mVideoFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
-                        getVerticalGridView().requestFocus();
-                        return true;
+                        if (getVerticalGridView().getChildCount() > 0) {
+                            getVerticalGridView().requestFocus();
+                            return true;
+                        }
                     }
                 }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index 625a196..f6f389c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -204,6 +204,10 @@
                 bottomDrawable,
                 coverDrawableParallaxTarget);
         mFragment.setBackgroundDrawable(mParallaxDrawable);
+        // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
+        // before PlaybackGlue is ready.
+        mVideoHelper = new DetailsBackgroundVideoHelper(null,
+                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
     }
 
     /**
@@ -227,12 +231,7 @@
             mPlaybackGlue.setHost(null);
         }
         mPlaybackGlue = playbackGlue;
-        if (mVideoHelper == null && mPlaybackGlue != null) {
-            mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
-                    mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
-        } else if (mVideoHelper != null) {
-            mVideoHelper.setPlaybackGlue(mPlaybackGlue);
-        }
+        mVideoHelper.setPlaybackGlue(mPlaybackGlue);
         if (mCanUseHost && mPlaybackGlue != null) {
             mPlaybackGlue.setHost(onCreateGlueHost());
         }
@@ -259,23 +258,36 @@
         return mPlaybackGlue != null;
     }
 
+    void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
+        mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+    }
+
     /**
      * Switch to video fragment, note that this method is not affected by result of
-     * {@link #canNavigateToVideoFragment()}.
+     * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()
+     * it will make video fragment to be initially focused once it is created.
+     * <p>
+     * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter
+     * transition and shared element transition.
+     * </p>
+     * <p>
+     * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and
+     * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.
+     * </p>
+     * <p>
+     * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an
+     * IllegalStateException will be thrown.
+     * </p>
      */
     public final void switchToVideo() {
-        if (mFragment.mVideoFragment != null && mFragment.mVideoFragment.getView() != null) {
-            mFragment.mVideoFragment.getView().requestFocus();
-        }
+        mFragment.switchToVideo();
     }
 
     /**
      * Switch to rows fragment.
      */
     public final void switchToRows() {
-        if (mFragment.mRowsFragment != null && mFragment.mRowsFragment.getView() != null) {
-            mFragment.mRowsFragment.getView().requestFocus();
-        }
+        mFragment.switchToRows();
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index 511a90b..1a74ce1 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -27,6 +27,8 @@
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
 import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -44,6 +46,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.Window;
 
 import java.lang.ref.WeakReference;
 
@@ -90,22 +93,190 @@
     static final String TAG = "DetailsSupportFragment";
     static boolean DEBUG = false;
 
+    final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            mRowsSupportFragment.setEntranceTransitionState(false);
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
+
+    void switchToVideoBeforeVideoSupportFragmentCreated() {
+        // if the video fragment is not ready: immediately fade out covering drawable,
+        // hide title and mark mPendingFocusOnVideo and set focus on it later.
+        mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+        showTitle(false);
+        mPendingFocusOnVideo = true;
+        slideOutGridView();
+    }
+
+    final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
+            false, false) {
+        @Override
+        public void run() {
+            switchToVideoBeforeVideoSupportFragmentCreated();
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
+            false, false) {
+        @Override
+        public void run() {
+            if (mWaitEnterTransitionTimeout != null) {
+                mWaitEnterTransitionTimeout.mRef.clear();
+            }
+            // clear the activity enter/sharedElement transition, return transitions are kept.
+            // keep the return transitions and clear enter transition
+            if (getActivity() != null) {
+                Window window = getActivity().getWindow();
+                Object returnTransition = TransitionHelper.getReturnTransition(window);
+                Object sharedReturnTransition = TransitionHelper
+                        .getSharedElementReturnTransition(window);
+                TransitionHelper.setEnterTransition(window, null);
+                TransitionHelper.setSharedElementEnterTransition(window, null);
+                TransitionHelper.setReturnTransition(window, returnTransition);
+                TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
+            }
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
+            true, false);
+
+    final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
+        @Override
+        public void run() {
+            Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
+            TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+        }
+    };
+
+    final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
+        @Override
+        public void run() {
+            if (mWaitEnterTransitionTimeout == null) {
+                new WaitEnterTransitionTimeout(DetailsSupportFragment.this);
+            }
+        }
+    };
+
     /**
-     * Flag for "possibly" having enter transition not finished yet.
-     * @see #mStartAndTransitionFlag
+     * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+     * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
      */
-    static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
-    /**
-     * Flag for having entrance transition not finished yet.
-     * @see #mStartAndTransitionFlag
-     */
-    static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
-    /**
-     * Flag that onStart() has been called and about to call onSafeStart() when
-     * pending transitions are finished.
-     * @see #mStartAndTransitionFlag
-     */
-    static final int PF_PENDING_START = 0x1 << 2;
+    static class WaitEnterTransitionTimeout implements Runnable {
+        static final long WAIT_ENTERTRANSITION_START = 200;
+
+        final WeakReference<DetailsSupportFragment> mRef;
+
+        WaitEnterTransitionTimeout(DetailsSupportFragment f) {
+            mRef = new WeakReference(f);
+            f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+        }
+
+        @Override
+        public void run() {
+            DetailsSupportFragment f = mRef.get();
+            if (f != null) {
+                f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
+            }
+        }
+    }
+
+    final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
+        @Override
+        public void run() {
+            onSafeStart();
+        }
+    };
+
+    final Event EVT_ONSTART = new Event("onStart");
+
+    final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
+
+    final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
+
+    final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
+
+    final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+        mStateMachine.addState(STATE_ON_SAFE_START);
+        mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
+        mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        /**
+         * Part 1: Processing enter transitions after fragment.onCreate
+         */
+        mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
+        // if transition is not supported, skip to complete
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+                COND_TRANSITION_NOT_SUPPORTED);
+        // if transition is not set on Activity, skip to complete
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+                EVT_NO_ENTER_TRANSITION);
+        // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
+        // complete.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
+                EVT_SWITCH_TO_VIDEO);
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
+        // once after onCreateView, we cannot skip the enter transition, add a listener and wait
+        // it to finish
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
+                EVT_ON_CREATEVIEW);
+        // when enter transition finishes, go to complete, however this might never happen if
+        // the activity is not giving transition options in startActivity, there is no API to query
+        // if this activity is started in a enter transition mode. So we rely on a timer below:
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+                STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
+        // we are expecting app to start delayed enter transition shortly after details row is
+        // loaded, so create a timer and wait for enter transition start.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+                STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
+        // if enter transition not started in the timer, skip to DONE, this can be also true when
+        // startActivity is not giving transition option.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
+                EVT_ENTER_TRANSIITON_DONE);
+
+        /**
+         * Part 2: modification to the entrance transition defined in BaseSupportFragment
+         */
+        // Must finish enter transition before perform entrance transition.
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
+        // Calling switch to video would hide immediately and skip entrance transition
+        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+                EVT_SWITCH_TO_VIDEO);
+        mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
+        // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
+        // still need to do the switchToVideo.
+        mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+                EVT_SWITCH_TO_VIDEO);
+
+        // for once the view is created in onStart and prepareEntranceTransition was called, we
+        // could setEntranceStartState:
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
+
+        /**
+         * Part 3: onSafeStart()
+         */
+        // for onSafeStart: the condition is onStart called, entrance transition complete
+        mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
+        mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
+        mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
+    }
 
     private class SetSelectionRunnable implements Runnable {
         int mPosition;
@@ -123,33 +294,6 @@
         }
     }
 
-    /**
-     * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
-     * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
-     * @see #mStartAndTransitionFlag
-     */
-    static class WaitEnterTransitionTimeout implements Runnable {
-        static final long WAIT_ENTERTRANSITION_START = 200;
-
-        final WeakReference<DetailsSupportFragment> mRef;
-
-        WaitEnterTransitionTimeout(DetailsSupportFragment f) {
-            mRef = new WeakReference(f);
-            f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
-        }
-
-        @Override
-        public void run() {
-            DetailsSupportFragment f = mRef.get();
-            if (f != null) {
-                f.clearPendingEnterTransition();
-            }
-        }
-    }
-
-    /**
-     * @see #mStartAndTransitionFlag
-     */
     TransitionListener mEnterTransitionListener = new TransitionListener() {
         @Override
         public void onTransitionStart(Object transition) {
@@ -162,12 +306,12 @@
 
         @Override
         public void onTransitionCancel(Object transition) {
-            clearPendingEnterTransition();
+            mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
         }
 
         @Override
         public void onTransitionEnd(Object transition) {
-            clearPendingEnterTransition();
+            mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
         }
     };
 
@@ -190,23 +334,10 @@
     BaseOnItemViewClickedListener mOnItemViewClickedListener;
     DetailsSupportFragmentBackgroundController mDetailsBackgroundController;
 
+    // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
+    // true, we will focus to VideoSupportFragment immediately after video fragment's view is created.
+    boolean mPendingFocusOnVideo = false;
 
-    /**
-     * Flags for enter transition, entrance transition and onStart.  When onStart() is called
-     * and both enter transiton and entrance transition are finished, we could call onSafeStart().
-     * 1. in onCreate:
-     *      if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
-     *      if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
-     *      user will run enterTransition or not.
-     * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
-     * start. If enter transition onTransitionStart is not invoked with a period, we can assume
-     * there is no enter transition running, then WaitEnterTransitionTimeout will clear
-     * PF_ENTER_TRANSITION_PENDING.
-     * 3. When enterTransition runs (either postponed or not),  we will stop the
-     * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
-     * PF_ENTER_TRANSITION_PENDING.
-     */
-    int mStartAndTransitionFlag = 0;
     WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
 
     Object mSceneAfterEntranceTransition;
@@ -290,14 +421,15 @@
         FragmentActivity activity = getActivity();
         if (activity != null) {
             Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
-            if (transition != null) {
-                mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
-                TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+            if (transition == null) {
+                mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
             }
             transition = TransitionHelper.getReturnTransition(activity.getWindow());
             if (transition != null) {
                 TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
             }
+        } else {
+            mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
         }
     }
 
@@ -452,6 +584,22 @@
         }
     }
 
+    void switchToVideo() {
+        if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null) {
+            mVideoSupportFragment.getView().requestFocus();
+        } else {
+            mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
+        }
+    }
+
+    void switchToRows() {
+        mPendingFocusOnVideo = false;
+        VerticalGridView verticalGridView = getVerticalGridView();
+        if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
+            verticalGridView.requestFocus();
+        }
+    }
+
     /**
      * This method asks DetailsSupportFragmentBackgroundController to add a fragment for rendering video.
      * In case the fragment is already there, it will return the existing one. The method must be
@@ -468,6 +616,18 @@
             ft2.add(android.support.v17.leanback.R.id.video_surface_container,
                     fragment = mDetailsBackgroundController.onCreateVideoSupportFragment());
             ft2.commit();
+            if (mPendingFocusOnVideo) {
+                // wait next cycle for Fragment view created so we can focus on it.
+                // This is a bit hack eventually we will do commitNow() which get view immediately.
+                getView().post(new Runnable() {
+                    public void run() {
+                        if (getView() != null) {
+                            switchToVideo();
+                        }
+                        mPendingFocusOnVideo = false;
+                    }
+                });
+            }
         }
         mVideoSupportFragment = fragment;
         return mVideoSupportFragment;
@@ -476,7 +636,7 @@
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
         if (( mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
-                && mRowsSupportFragment.getView().hasFocus())
+                && mRowsSupportFragment.getView().hasFocus() && !mPendingFocusOnVideo)
                 && (adapter == null || adapter.size() == 0
                 || (getVerticalGridView().getSelectedPosition() == 0
                 && getVerticalGridView().getSelectedSubPosition() == 0))) {
@@ -487,10 +647,8 @@
         if (adapter != null && adapter.size() > selectedPosition) {
             final VerticalGridView gridView = getVerticalGridView();
             final int count = gridView.getChildCount();
-            if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
-                if (mWaitEnterTransitionTimeout == null) {
-                    mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
-                }
+            if (count > 0) {
+                mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
             }
             for (int i = 0; i < count; i++) {
                 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
@@ -504,25 +662,6 @@
         }
     }
 
-    void clearPendingEnterTransition() {
-        if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
-            mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
-            dispatchOnStartAndTransitionFinished();
-        }
-    }
-
-    void dispatchOnStartAndTransitionFinished() {
-        /**
-         * if onStart() was called and there is no pending enter transition or entrance transition.
-         */
-        if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
-                && (mStartAndTransitionFlag
-                & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
-            mStartAndTransitionFlag &= ~PF_PENDING_START;
-            onSafeStart();
-        }
-    }
-
     /**
      * Called when onStart and enter transition (postponed/none postponed) and entrance transition
      * are all finished.
@@ -617,17 +756,14 @@
     public void onStart() {
         super.onStart();
 
-        mStartAndTransitionFlag |= PF_PENDING_START;
-        dispatchOnStartAndTransitionFinished();
-
         setupChildFragmentLayout();
-        if (isEntranceTransitionEnabled()) {
-            mRowsSupportFragment.setEntranceTransitionState(false);
-        }
+        mStateMachine.fireEvent(EVT_ONSTART);
         if (mDetailsParallax != null) {
             mDetailsParallax.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
         }
-        if (!getView().hasFocus()) {
+        if (mPendingFocusOnVideo) {
+            slideOutGridView();
+        } else if (!getView().hasFocus()) {
             mRowsSupportFragment.getVerticalGridView().requestFocus();
         }
     }
@@ -645,14 +781,11 @@
 
     @Override
     protected void onEntranceTransitionEnd() {
-        mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
-        dispatchOnStartAndTransitionFinished();
         mRowsSupportFragment.onTransitionEnd();
     }
 
     @Override
     protected void onEntranceTransitionPrepare() {
-        mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
         mRowsSupportFragment.onTransitionPrepare();
     }
 
@@ -714,8 +847,10 @@
             public void onRequestChildFocus(View child, View focused) {
                 if (child != mRootView.getFocusedChild()) {
                     if (child.getId() == R.id.details_fragment_root) {
-                        slideInGridView();
-                        showTitle(true);
+                        if (!mPendingFocusOnVideo) {
+                            slideInGridView();
+                            showTitle(true);
+                        }
                     } else if (child.getId() == R.id.video_surface_container) {
                         slideOutGridView();
                         showTitle(false);
@@ -761,8 +896,10 @@
                 if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
                         && mVideoSupportFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
-                        getVerticalGridView().requestFocus();
-                        return true;
+                        if (getVerticalGridView().getChildCount() > 0) {
+                            getVerticalGridView().requestFocus();
+                            return true;
+                        }
                     }
                 }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
index 6149625..763b84f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -207,6 +207,10 @@
                 bottomDrawable,
                 coverDrawableParallaxTarget);
         mFragment.setBackgroundDrawable(mParallaxDrawable);
+        // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
+        // before PlaybackGlue is ready.
+        mVideoHelper = new DetailsBackgroundVideoHelper(null,
+                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
     }
 
     /**
@@ -230,12 +234,7 @@
             mPlaybackGlue.setHost(null);
         }
         mPlaybackGlue = playbackGlue;
-        if (mVideoHelper == null && mPlaybackGlue != null) {
-            mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
-                    mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
-        } else if (mVideoHelper != null) {
-            mVideoHelper.setPlaybackGlue(mPlaybackGlue);
-        }
+        mVideoHelper.setPlaybackGlue(mPlaybackGlue);
         if (mCanUseHost && mPlaybackGlue != null) {
             mPlaybackGlue.setHost(onCreateGlueHost());
         }
@@ -262,23 +261,36 @@
         return mPlaybackGlue != null;
     }
 
+    void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
+        mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+    }
+
     /**
      * Switch to video fragment, note that this method is not affected by result of
-     * {@link #canNavigateToVideoSupportFragment()}.
+     * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate()
+     * it will make video fragment to be initially focused once it is created.
+     * <p>
+     * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter
+     * transition and shared element transition.
+     * </p>
+     * <p>
+     * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and
+     * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored.
+     * </p>
+     * <p>
+     * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an
+     * IllegalStateException will be thrown.
+     * </p>
      */
     public final void switchToVideo() {
-        if (mFragment.mVideoSupportFragment != null && mFragment.mVideoSupportFragment.getView() != null) {
-            mFragment.mVideoSupportFragment.getView().requestFocus();
-        }
+        mFragment.switchToVideo();
     }
 
     /**
      * Switch to rows fragment.
      */
     public final void switchToRows() {
-        if (mFragment.mRowsSupportFragment != null && mFragment.mRowsSupportFragment.getView() != null) {
-            mFragment.mRowsSupportFragment.getView().requestFocus();
-        }
+        mFragment.switchToRows();
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
index d3a45a0..fe0e26f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -349,6 +349,10 @@
             if (mExternalAdapterListener != null) {
                 mExternalAdapterListener.onCreate(vh);
             }
+            RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+            RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+            rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+            rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
         }
 
         @Override
@@ -362,8 +366,6 @@
             setRowViewExpanded(vh, mExpand);
             RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
             RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
-            rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
-            rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
             rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
             if (mExternalAdapterListener != null) {
                 mExternalAdapterListener.onAttachedToWindow(vh);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
index 5582644..9f55aa2 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
@@ -352,6 +352,10 @@
             if (mExternalAdapterListener != null) {
                 mExternalAdapterListener.onCreate(vh);
             }
+            RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+            RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+            rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+            rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
         }
 
         @Override
@@ -365,8 +369,6 @@
             setRowViewExpanded(vh, mExpand);
             RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
             RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
-            rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
-            rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
             rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
             if (mExternalAdapterListener != null) {
                 mExternalAdapterListener.onAttachedToWindow(vh);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
index cfa27df..9e80dfc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -16,6 +16,7 @@
 import android.os.Bundle;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.OnChildLaidOutListener;
@@ -49,6 +50,29 @@
     private int mSelectedPosition = -1;
 
     /**
+     * State to setEntranceTransitionState(false)
+     */
+    final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            setEntranceTransitionState(false);
+        }
+    };
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
+    }
+
+    /**
      * Sets the grid presenter.
      */
     public void setGridPresenter(VerticalGridPresenter gridPresenter) {
@@ -160,13 +184,8 @@
         ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
         installTitleView(inflater, gridFrame, savedInstanceState);
         getProgressBarManager().setRootView(root);
-        return root;
-    }
 
-    @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock);
+        ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
         mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
         gridDock.addView(mGridViewHolder.view);
         mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
@@ -179,6 +198,7 @@
         });
 
         updateAdapter();
+        return root;
     }
 
     private void setupFocusSearchListener() {
@@ -191,9 +211,6 @@
     public void onStart() {
         super.onStart();
         setupFocusSearchListener();
-        if (isEntranceTransitionEnabled()) {
-            setEntranceTransitionState(false);
-        }
     }
 
     @Override
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
index 55e079d..6327790 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
@@ -19,6 +19,7 @@
 import android.os.Bundle;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine.State;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.OnChildLaidOutListener;
@@ -52,6 +53,29 @@
     private int mSelectedPosition = -1;
 
     /**
+     * State to setEntranceTransitionState(false)
+     */
+    final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+        @Override
+        public void run() {
+            setEntranceTransitionState(false);
+        }
+    };
+
+    @Override
+    void createStateMachineStates() {
+        super.createStateMachineStates();
+        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+    }
+
+    @Override
+    void createStateMachineTransitions() {
+        super.createStateMachineTransitions();
+        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+                STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
+    }
+
+    /**
      * Sets the grid presenter.
      */
     public void setGridPresenter(VerticalGridPresenter gridPresenter) {
@@ -163,13 +187,8 @@
         ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
         installTitleView(inflater, gridFrame, savedInstanceState);
         getProgressBarManager().setRootView(root);
-        return root;
-    }
 
-    @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock);
+        ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
         mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
         gridDock.addView(mGridViewHolder.view);
         mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
@@ -182,6 +201,7 @@
         });
 
         updateAdapter();
+        return root;
     }
 
     private void setupFocusSearchListener() {
@@ -194,9 +214,6 @@
     public void onStart() {
         super.onStart();
         setupFocusSearchListener();
-        if (isEntranceTransitionEnabled()) {
-            setEntranceTransitionState(false);
-        }
     }
 
     @Override
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
index 1da1bda..48aaebe 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -27,6 +27,7 @@
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.PlaybackControlsRow;
 import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
 import android.support.v17.leanback.widget.PresenterSelector;
 import android.support.v17.leanback.widget.RowPresenter;
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
@@ -54,8 +55,10 @@
  * inform the glue what speed levels are supported for fast forward/rewind.
  * </p>
  *
- * <p>You may override {@link #onCreateControlsRowAndPresenter()} which will set a controls
- * row and return a row presenter you can use to present the row.
+ * <p>You may override {@link #onCreateControlsRowAndPresenter()} which will create a
+ * {@link PlaybackControlsRow} and a {@link PlaybackControlsRowPresenter}. You may call
+ * {@link #setControlsRow(PlaybackControlsRow)} and
+ * {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to customize your own row and presenter.
  * </p>
  *
  * <p>The helper sets a {@link SparseArrayObjectAdapter}
@@ -176,7 +179,7 @@
     private final int[] mFastForwardSpeeds;
     private final int[] mRewindSpeeds;
     private PlaybackControlsRow mControlsRow;
-    private PlaybackControlsRowPresenter mControlsRowPresenter;
+    private PlaybackRowPresenter mControlsRowPresenter;
     private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
     private PlaybackControlsRow.SkipNextAction mSkipNextAction;
     private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
@@ -237,10 +240,10 @@
         super.onAttachedToHost(host);
         host.setOnKeyInterceptListener(this);
         host.setOnActionClickedListener(this);
-        if (getControlsRow() == null || getControlsRowPresenter() == null) {
+        if (getControlsRow() == null || getPlaybackRowPresenter() == null) {
             onCreateControlsRowAndPresenter();
         }
-        host.setPlaybackRowPresenter(getControlsRowPresenter());
+        host.setPlaybackRowPresenter(getPlaybackRowPresenter());
         host.setPlaybackRow(getControlsRow());
     }
 
@@ -265,37 +268,40 @@
      * {@link PlaybackControlsRowPresenter}. Subclass may override.
      */
     protected void onCreateControlsRowAndPresenter() {
-        PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
-        setControlsRow(controlsRow);
+        if (getControlsRow() == null) {
+            PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+            setControlsRow(controlsRow);
+        }
+        if (getPlaybackRowPresenter() == null) {
+            final AbstractDetailsDescriptionPresenter detailsPresenter =
+                    new AbstractDetailsDescriptionPresenter() {
+                        @Override
+                        protected void onBindDescription(ViewHolder
+                                viewHolder, Object object) {
+                            PlaybackControlGlue glue = (PlaybackControlGlue) object;
+                            if (glue.hasValidMedia()) {
+                                viewHolder.getTitle().setText(glue.getMediaTitle());
+                                viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
+                            } else {
+                                viewHolder.getTitle().setText("");
+                                viewHolder.getSubtitle().setText("");
+                            }
+                        }
+                    };
 
-        final AbstractDetailsDescriptionPresenter detailsPresenter =
-                new AbstractDetailsDescriptionPresenter() {
-            @Override
-            protected void onBindDescription(ViewHolder
-                                                     viewHolder, Object object) {
-                PlaybackControlGlue glue = (PlaybackControlGlue) object;
-                if (glue.hasValidMedia()) {
-                    viewHolder.getTitle().setText(glue.getMediaTitle());
-                    viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
-                } else {
-                    viewHolder.getTitle().setText("");
-                    viewHolder.getSubtitle().setText("");
+            setPlaybackRowPresenter(new PlaybackControlsRowPresenter(detailsPresenter) {
+                @Override
+                protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
+                    super.onBindRowViewHolder(vh, item);
+                    vh.setOnKeyListener(PlaybackControlGlue.this);
                 }
-            }
-        };
-
-        setControlsRowPresenter(new PlaybackControlsRowPresenter(detailsPresenter) {
-            @Override
-            protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
-                super.onBindRowViewHolder(vh, item);
-                vh.setOnKeyListener(PlaybackControlGlue.this);
-            }
-            @Override
-            protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
-                super.onUnbindRowViewHolder(vh);
-                vh.setOnKeyListener(null);
-            }
-        });
+                @Override
+                protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
+                    super.onUnbindRowViewHolder(vh);
+                    vh.setOnKeyListener(null);
+                }
+            });
+        }
     }
 
     /**
@@ -358,7 +364,10 @@
 
     /**
      * Sets the controls row Presenter to be managed by the glue layer.
+     * @deprecated PlaybackControlGlue supports any PlaybackRowPresenter, use
+     * {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}.
      */
+    @Deprecated
     public void setControlsRowPresenter(PlaybackControlsRowPresenter presenter) {
         mControlsRowPresenter = presenter;
     }
@@ -372,8 +381,28 @@
 
     /**
      * Returns the playback controls row Presenter managed by the glue layer.
+     * @deprecated PlaybackControlGlue supports any PlaybackRowPresenter, use
+     * {@link #getPlaybackRowPresenter()}.
      */
+    @Deprecated
     public PlaybackControlsRowPresenter getControlsRowPresenter() {
+        return mControlsRowPresenter instanceof PlaybackControlsRowPresenter
+                ? (PlaybackControlsRowPresenter) mControlsRowPresenter : null;
+    }
+
+    /**
+     * Sets the controls row Presenter to be passed to {@link PlaybackGlueHost} in
+     * {@link #onAttachedToHost(PlaybackGlueHost)}.
+     */
+    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+        mControlsRowPresenter = presenter;
+    }
+
+    /**
+     * Returns the playback row Presenter to be passed to {@link PlaybackGlueHost} in
+     * {@link #onAttachedToHost(PlaybackGlueHost)}.
+     */
+    public PlaybackRowPresenter getPlaybackRowPresenter() {
         return mControlsRowPresenter;
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
index bef3d8d..9f92066 100644
--- a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
@@ -62,100 +62,108 @@
     /**
      * Interface implemented by classes that support Transition animations.
      */
-    static interface TransitionHelperVersionImpl {
+    interface TransitionHelperVersionImpl {
 
-        public void setEnterTransition(android.app.Fragment fragment, Object transition);
+        void setEnterTransition(android.app.Fragment fragment, Object transition);
 
-        public void setExitTransition(android.app.Fragment fragment, Object transition);
+        void setExitTransition(android.app.Fragment fragment, Object transition);
 
-        public void setSharedElementEnterTransition(android.app.Fragment fragment,
+        void setSharedElementEnterTransition(android.app.Fragment fragment,
                 Object transition);
 
-        public void addSharedElement(android.app.FragmentTransaction ft,
+        void addSharedElement(android.app.FragmentTransaction ft,
                 View view, String transitionName);
 
-        public Object getSharedElementEnterTransition(Window window);
+        Object getSharedElementEnterTransition(Window window);
 
-        public Object getSharedElementReturnTransition(Window window);
+        void setSharedElementEnterTransition(Window window, Object transition);
 
-        public Object getSharedElementExitTransition(Window window);
+        Object getSharedElementReturnTransition(Window window);
 
-        public Object getSharedElementReenterTransition(Window window);
+        void setSharedElementReturnTransition(Window window, Object transition);
 
-        public Object getEnterTransition(Window window);
+        Object getSharedElementExitTransition(Window window);
 
-        public Object getReturnTransition(Window window);
+        Object getSharedElementReenterTransition(Window window);
 
-        public Object getExitTransition(Window window);
+        Object getEnterTransition(Window window);
 
-        public Object getReenterTransition(Window window);
+        void setEnterTransition(Window window, Object transition);
 
-        public Object createScene(ViewGroup sceneRoot, Runnable r);
+        Object getReturnTransition(Window window);
 
-        public Object createAutoTransition();
+        void setReturnTransition(Window window, Object transition);
 
-        public Object createSlide(int slideEdge);
+        Object getExitTransition(Window window);
 
-        public Object createScale();
+        Object getReenterTransition(Window window);
 
-        public Object createFadeTransition(int fadingMode);
+        Object createScene(ViewGroup sceneRoot, Runnable r);
 
-        public Object createChangeTransform();
+        Object createAutoTransition();
 
-        public Object createChangeBounds(boolean reparent);
+        Object createSlide(int slideEdge);
 
-        public Object createFadeAndShortSlide(int edge);
+        Object createScale();
 
-        public Object createFadeAndShortSlide(int edge, float distance);
+        Object createFadeTransition(int fadingMode);
 
-        public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay);
+        Object createChangeTransform();
 
-        public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay);
+        Object createChangeBounds(boolean reparent);
 
-        public void setChangeBoundsStartDelay(Object changeBounds, String className,
+        Object createFadeAndShortSlide(int edge);
+
+        Object createFadeAndShortSlide(int edge, float distance);
+
+        void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay);
+
+        void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay);
+
+        void setChangeBoundsStartDelay(Object changeBounds, String className,
                 int startDelay);
 
-        public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay);
+        void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay);
 
-        public Object createTransitionSet(boolean sequential);
+        Object createTransitionSet(boolean sequential);
 
-        public void addTransition(Object transitionSet, Object transition);
+        void addTransition(Object transitionSet, Object transition);
 
-        public void addTransitionListener(Object transition, TransitionListener listener);
+        void addTransitionListener(Object transition, TransitionListener listener);
 
-        public void removeTransitionListener(Object transition, TransitionListener listener);
+        void removeTransitionListener(Object transition, TransitionListener listener);
 
-        public void runTransition(Object scene, Object transition);
+        void runTransition(Object scene, Object transition);
 
-        public void exclude(Object transition, int targetId, boolean exclude);
+        void exclude(Object transition, int targetId, boolean exclude);
 
-        public void exclude(Object transition, View targetView, boolean exclude);
+        void exclude(Object transition, View targetView, boolean exclude);
 
-        public void excludeChildren(Object transition, int targetId, boolean exclude);
+        void excludeChildren(Object transition, int targetId, boolean exclude);
 
-        public void excludeChildren(Object transition, View target, boolean exclude);
+        void excludeChildren(Object transition, View target, boolean exclude);
 
-        public void include(Object transition, int targetId);
+        void include(Object transition, int targetId);
 
-        public void include(Object transition, View targetView);
+        void include(Object transition, View targetView);
 
-        public void setStartDelay(Object transition, long startDelay);
+        void setStartDelay(Object transition, long startDelay);
 
-        public void setDuration(Object transition, long duration);
+        void setDuration(Object transition, long duration);
 
-        public void setInterpolator(Object transition, Object timeInterpolator);
+        void setInterpolator(Object transition, Object timeInterpolator);
 
-        public void addTarget(Object transition, View view);
+        void addTarget(Object transition, View view);
 
-        public Object createDefaultInterpolator(Context context);
+        Object createDefaultInterpolator(Context context);
 
-        public Object loadTransition(Context context, int resId);
+        Object loadTransition(Context context, int resId);
 
-        public void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject);
+        void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject);
 
-        public void setTransitionGroup(ViewGroup viewGroup, boolean transitionGroup);
+        void setTransitionGroup(ViewGroup viewGroup, boolean transitionGroup);
 
-        public void setEpicenterCallback(Object transitionObject,
+        void setEpicenterCallback(Object transitionObject,
                 TransitionEpicenterCallback callback);
     }
 
@@ -195,11 +203,19 @@
         }
 
         @Override
+        public void setSharedElementEnterTransition(Window window, Object object) {
+        }
+
+        @Override
         public Object getSharedElementReturnTransition(Window window) {
             return null;
         }
 
         @Override
+        public void setSharedElementReturnTransition(Window window, Object transition) {
+        }
+
+        @Override
         public Object getSharedElementExitTransition(Window window) {
             return null;
         }
@@ -215,11 +231,19 @@
         }
 
         @Override
+        public void setEnterTransition(Window window, Object transition) {
+        }
+
+        @Override
         public Object getReturnTransition(Window window) {
             return null;
         }
 
         @Override
+        public void setReturnTransition(Window window, Object transition) {
+        }
+
+        @Override
         public Object getExitTransition(Window window) {
             return null;
         }
@@ -572,11 +596,21 @@
         }
 
         @Override
+        public void setSharedElementEnterTransition(Window window, Object object) {
+            TransitionHelperApi21.setSharedElementEnterTransition(window, object);
+        }
+
+        @Override
         public Object getSharedElementReturnTransition(Window window) {
             return TransitionHelperApi21.getSharedElementReturnTransition(window);
         }
 
         @Override
+        public void setSharedElementReturnTransition(Window window, Object transition) {
+            TransitionHelperApi21.setSharedElementReturnTransition(window, transition);
+        }
+
+        @Override
         public Object getSharedElementExitTransition(Window window) {
             return TransitionHelperApi21.getSharedElementExitTransition(window);
         }
@@ -607,11 +641,21 @@
         }
 
         @Override
+        public void setEnterTransition(Window window, Object transition) {
+            TransitionHelperApi21.setEnterTransition(window, transition);
+        }
+
+        @Override
         public Object getReturnTransition(Window window) {
             return TransitionHelperApi21.getReturnTransition(window);
         }
 
         @Override
+        public void setReturnTransition(Window window, Object transition) {
+            TransitionHelperApi21.setReturnTransition(window, transition);
+        }
+
+        @Override
         public Object getExitTransition(Window window) {
             return TransitionHelperApi21.getExitTransition(window);
         }
@@ -662,10 +706,18 @@
         return sImpl.getSharedElementEnterTransition(window);
     }
 
+    public static void setSharedElementEnterTransition(Window window, Object transition) {
+        sImpl.setSharedElementEnterTransition(window, transition);
+    }
+
     public static Object getSharedElementReturnTransition(Window window) {
         return sImpl.getSharedElementReturnTransition(window);
     }
 
+    public static void setSharedElementReturnTransition(Window window, Object transition) {
+        sImpl.setSharedElementReturnTransition(window, transition);
+    }
+
     public static Object getSharedElementExitTransition(Window window) {
         return sImpl.getSharedElementExitTransition(window);
     }
@@ -678,10 +730,18 @@
         return sImpl.getEnterTransition(window);
     }
 
+    public static void setEnterTransition(Window window, Object transition) {
+        sImpl.setEnterTransition(window, transition);
+    }
+
     public static Object getReturnTransition(Window window) {
         return sImpl.getReturnTransition(window);
     }
 
+    public static void setReturnTransition(Window window, Object transition) {
+        sImpl.setReturnTransition(window, transition);
+    }
+
     public static Object getExitTransition(Window window) {
         return sImpl.getExitTransition(window);
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java b/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
index b9d2f2d..dfc228c 100644
--- a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
+++ b/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
@@ -16,42 +16,169 @@
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.support.annotation.RestrictTo;
+import android.util.Log;
 
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
 
 /**
- * Linear or DAG of {@link State}s. StateMachine is by default a linear model, until
- * {@link #addState(State, State)} is called.  Each State has three status:
- * STATUS_ZERO, STATUS_INVOKED, STATUS_EXECUTED.   We allow client to run a State, which will
- * put State in STATUS_INVOKED.  A State will be executed when prior States are executed and
- * Precondition for this State is true, then the State will be marked as STATUS_EXECUTED.
- *
+ * State: each State has incoming Transitions and outgoing Transitions.
+ * When {@link State#mBranchStart} is true, all the outgoing Transitions may be triggered, when
+ * {@link State#mBranchStart} is false, only first outgoing Transition will be triggered.
+ * When {@link State#mBranchEnd} is true, all the incoming Transitions must be triggered for the
+ * State to run. When {@link State#mBranchEnd} is false, only need one incoming Transition triggered
+ * for the State to run.
+ * Transition: three types:
+ * 1. Event based transition, transition will be triggered when {@link #fireEvent(Event)} is called.
+ * 2. Auto transition, transition will be triggered when {@link Transition#mFromState} is executed.
+ * 3. Condiitonal Auto transition, transition will be triggered when {@link Transition#mFromState}
+ * is executed and {@link Transition#mCondition} passes.
  * @hide
  */
 @RestrictTo(LIBRARY_GROUP)
 public final class StateMachine {
 
+    static boolean DEBUG = false;
+    static final String TAG = "StateMachine";
+
     /**
      * No request on the State
      */
     public static final int STATUS_ZERO = 0;
+
     /**
-     * Somebody wants to run the state but not yet executed because either the condition is
-     * false or lower States are not executed.
+     * Has been executed
      */
     public static final int STATUS_INVOKED = 1;
-    /**
-     * Somebody wants to run the State and the State was executed.
-     */
-    public static final int STATUS_EXECUTED = 2;
 
+    /**
+     * Used in Transition
+     */
+    public static class Event {
+        final String mName;
+
+        public Event(String name) {
+            mName = name;
+        }
+    }
+
+    /**
+     * Used in transition
+     */
+    public static class Condition {
+        final String mName;
+
+        public Condition(String name) {
+            mName = name;
+        }
+
+        /**
+         * @return True if can proceed and mark the transition INVOKED
+         */
+        public boolean canProceed() {
+            return true;
+        }
+    }
+
+    static class Transition {
+        final State mFromState;
+        final State mToState;
+        final Event mEvent;
+        final Condition mCondition;
+        int mState = STATUS_ZERO;
+
+        Transition(State fromState, State toState, Event event) {
+            if (event == null) {
+                throw new IllegalArgumentException();
+            }
+            mFromState = fromState;
+            mToState = toState;
+            mEvent = event;
+            mCondition = null;
+        }
+
+        Transition(State fromState, State toState) {
+            mFromState = fromState;
+            mToState = toState;
+            mEvent = null;
+            mCondition = null;
+        }
+
+        Transition(State fromState, State toState, Condition condition) {
+            if (condition == null) {
+                throw new IllegalArgumentException();
+            }
+            mFromState = fromState;
+            mToState = toState;
+            mEvent = null;
+            mCondition = condition;
+        }
+
+        @Override
+        public String toString() {
+            String signalName;
+            if (mEvent != null) {
+                signalName = mEvent.mName;
+            } else if (mCondition != null) {
+                signalName = mCondition.mName;
+            } else {
+                signalName = "auto";
+            }
+            return "[" + mFromState.mName + " -> " + mToState.mName + " <"
+                    + signalName + ">]";
+        }
+    }
+
+    /**
+     * @see StateMachine
+     */
     public static class State {
 
-        private int mStatus;
-        ArrayList<State> mPriorStates;
+        final String mName;
+        final boolean mBranchStart;
+        final boolean mBranchEnd;
+        int mStatus = STATUS_ZERO;
+        int mInvokedOutTransitions = 0;
+        ArrayList<Transition> mIncomings;
+        ArrayList<Transition> mOutgoings;
+
+        @Override
+        public String toString() {
+            return "[" + mName + " " + mStatus + "]";
+        }
+
+        /**
+         * Create a State which is not branch start and a branch end.
+         */
+        public State(String name) {
+            this(name, false, true);
+        }
+
+        /**
+         * Create a State
+         * @param branchStart True if can run all out going transitions or false execute the first
+         *                    out going transition.
+         * @param branchEnd True if wait all incoming transitions executed or false
+         *                              only need one of the transition executed.
+         */
+        public State(String name, boolean branchStart, boolean branchEnd) {
+            mName = name;
+            mBranchStart = branchStart;
+            mBranchEnd = branchEnd;
+        }
+
+        void addIncoming(Transition t) {
+            if (mIncomings == null) {
+                mIncomings = new ArrayList();
+            }
+            mIncomings.add(t);
+        }
+
+        void addOutgoing(Transition t) {
+            if (mOutgoings == null) {
+                mOutgoings = new ArrayList();
+            }
+            mOutgoings.add(t);
+        }
 
         /**
          * Run State, Subclass may override.
@@ -59,51 +186,66 @@
         public void run() {
         }
 
-        /**
-         * Returns true if State can run, false otherwise.  Subclass may override.
-         * @return True if State can run, false otherwise.  Subclass may override.
-         */
-        public boolean canRun() {
-            return true;
+        final boolean checkPreCondition() {
+            if (mIncomings == null) {
+                return true;
+            }
+            if (mBranchEnd) {
+                for (Transition t: mIncomings) {
+                    if (t.mState != STATUS_INVOKED) {
+                        return false;
+                    }
+                }
+                return true;
+            } else {
+                for (Transition t: mIncomings) {
+                    if (t.mState == STATUS_INVOKED) {
+                        return true;
+                    }
+                }
+                return false;
+            }
         }
 
         /**
          * @return True if the State has been executed.
          */
         final boolean runIfNeeded() {
-            if (mStatus!= STATUS_EXECUTED) {
-                if (mStatus == STATUS_INVOKED && canRun()) {
+            if (mStatus != STATUS_INVOKED) {
+                if (checkPreCondition()) {
+                    if (DEBUG) {
+                        Log.d(TAG, "execute " + this);
+                    }
+                    mStatus = STATUS_INVOKED;
                     run();
-                    mStatus = STATUS_EXECUTED;
-                } else {
-                    return false;
+                    signalAutoTransitionsAfterRun();
+                    return true;
                 }
             }
-            return true;
+            return false;
         }
 
-        void addPriorState(State state) {
-            if (mPriorStates == null) {
-                mPriorStates = new ArrayList<State>();
+        final void signalAutoTransitionsAfterRun() {
+            if (mOutgoings != null) {
+                for (Transition t: mOutgoings) {
+                    if (t.mEvent == null) {
+                        if (t.mCondition == null || t.mCondition.canProceed()) {
+                            if (DEBUG) {
+                                Log.d(TAG, "signal " + t);
+                            }
+                            mInvokedOutTransitions++;
+                            t.mState = STATUS_INVOKED;
+                            if (!mBranchStart) {
+                                break;
+                            }
+                        }
+                    }
+                }
             }
-            if (!mPriorStates.contains(state)) {
-                mPriorStates.add(state);
-            }
-        }
-
-        final void markInvoked() {
-            if (mStatus == STATUS_ZERO) {
-                mStatus = STATUS_INVOKED;
-            }
-        }
-
-        final void updateStatus(int status) {
-            mStatus = status;
         }
 
         /**
-         * Get status, return one of {@link #STATUS_ZERO}, {@link #STATUS_INVOKED},
-         * {@link #STATUS_EXECUTED}.
+         * Get status, return one of {@link #STATUS_ZERO}, {@link #STATUS_INVOKED}.
          * @return Status of the State.
          */
         public final int getStatus() {
@@ -111,109 +253,127 @@
         }
     }
 
-    private boolean mSorted = true;
-    private final ArrayList<State> mSortedList = new ArrayList<State>();
+    final ArrayList<State> mStates = new ArrayList<State>();
+    final ArrayList<State> mFinishedStates = new ArrayList();
+    final ArrayList<State> mUnfinishedStates = new ArrayList();
+
+    public StateMachine() {
+    }
 
     /**
      * Add a State to StateMachine, ignore if it is already added.
      * @param state The state to add.
      */
     public void addState(State state) {
-        if (!mSortedList.contains(state)) {
-            state.updateStatus(STATUS_ZERO);
-            mSortedList.add(state);
+        if (!mStates.contains(state)) {
+            mStates.add(state);
         }
     }
 
     /**
-     * Add two States to StateMachine and create an edge between this two.
-     * StateMachine is by default a linear model, until {@link #addState(State, State)} is called.
-     * sort() is required to sort the Direct acyclic graph.
+     * Add event-triggered transition between two states.
+     * @param fromState The from state.
+     * @param toState The to state.
+     * @param event The event that needed to perform the transition.
+     */
+    public void addTransition(State fromState, State toState, Event event) {
+        Transition transition = new Transition(fromState, toState, event);
+        toState.addIncoming(transition);
+        fromState.addOutgoing(transition);
+    }
+
+    /**
+     * Add a conditional auto transition between two states.
+     * @param fromState The from state.
+     * @param toState The to state.
+     */
+    public void addTransition(State fromState, State toState, Condition condition) {
+        Transition transition = new Transition(fromState, toState, condition);
+        toState.addIncoming(transition);
+        fromState.addOutgoing(transition);
+    }
+
+    /**
+     * Add an auto transition between two states.
      * @param fromState The from state to add.
      * @param toState The to state to add.
      */
-    public void addState(State fromState, State toState) {
-        addState(fromState);
-        addState(toState);
-        toState.addPriorState(fromState);
-        mSorted = false;
-    }
-
-    void verifySorted() {
-        if (!mSorted) {
-            throw new RuntimeException("Graph not sorted");
-        }
-    }
-
-    public void runState(State state) {
-        verifySorted();
-        state.markInvoked();
-        runPendingStates();
-    }
-
-    public void runPendingStates() {
-        verifySorted();
-        for (int i = 0, size = mSortedList.size(); i < size; i++) {
-            if (!mSortedList.get(i).runIfNeeded()) {
-                break;
-            }
-        }
-    }
-
-    public void resetStatus() {
-        for (int i = 0, size = mSortedList.size(); i < size; i++) {
-            mSortedList.get(i).updateStatus(STATUS_ZERO);
-        }
+    public void addTransition(State fromState, State toState) {
+        Transition transition = new Transition(fromState, toState);
+        toState.addIncoming(transition);
+        fromState.addOutgoing(transition);
     }
 
     /**
-     * StateMachine is by default a linear model, until {@link #addState(State, State)} is called.
-     * sort() is required to sort the Direct acyclic graph.
+     * Start the state machine.
      */
-    public void sort() {
-        if (mSorted) {
-            return;
+    public void start() {
+        if (DEBUG) {
+            Log.d(TAG, "start");
         }
-        // L: Empty list that will contain the sorted States
-        ArrayList<State> L = new ArrayList<State>();
-        // S: Set of all nodes with no incoming edges
-        ArrayList<State> S = new ArrayList<State>();
-        HashMap<State, ArrayList<State>> edges = new HashMap<State, ArrayList<State>>();
-        for (int i = mSortedList.size() - 1; i >= 0 ; i--) {
-            State state = mSortedList.get(i);
-            if (state.mPriorStates != null && state.mPriorStates.size() > 0) {
-                edges.put(state, new ArrayList<State>(state.mPriorStates));
-            } else {
-                S.add(state);
-            }
-        }
+        mUnfinishedStates.addAll(mStates);
+        runUnfinishedStates();
+    }
 
-        while (!S.isEmpty()) {
-            // remove a State without incoming Node from S, add to L
-            State state = S.remove(S.size() - 1);
-            L.add(state);
-            // for each toState that having an incoming edge from "state":
-            for (Iterator<Map.Entry<State, ArrayList<State>>> iterator =
-                    edges.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<State, ArrayList<State>> entry = iterator.next();
-                ArrayList<State> fromStates = entry.getValue();
-                // remove edge from graph
-                if (fromStates.remove(state)) {
-                    if (fromStates.size() == 0) {
-                        State toState = entry.getKey();
-                        // insert the toState to S if it has no more incoming edges
-                        S.add(toState);
-                        iterator.remove();
+    void runUnfinishedStates() {
+        boolean changed;
+        do {
+            changed = false;
+            for (int i = mUnfinishedStates.size() - 1; i >= 0; i--) {
+                State state = mUnfinishedStates.get(i);
+                if (state.runIfNeeded()) {
+                    mUnfinishedStates.remove(i);
+                    mFinishedStates.add(state);
+                    changed = true;
+                }
+            }
+        } while (changed);
+    }
+
+    /**
+     * Find outgoing Transitions of invoked State whose Event matches, mark the Transition invoked.
+     */
+    public void fireEvent(Event event) {
+        for (int i = 0; i < mFinishedStates.size(); i++) {
+            State state = mFinishedStates.get(i);
+            if (state.mOutgoings != null) {
+                if (!state.mBranchStart && state.mInvokedOutTransitions > 0) {
+                    continue;
+                }
+                for (Transition t : state.mOutgoings) {
+                    if (t.mState != STATUS_INVOKED && t.mEvent == event) {
+                        if (DEBUG) {
+                            Log.d(TAG, "signal " + t);
+                        }
+                        t.mState = STATUS_INVOKED;
+                        state.mInvokedOutTransitions++;
+                        if (!state.mBranchStart) {
+                            break;
+                        }
                     }
                 }
             }
         }
-        if (edges.size() > 0) {
-            throw new RuntimeException("Cycle in Graph");
-        }
+        runUnfinishedStates();
+    }
 
-        mSortedList.clear();
-        mSortedList.addAll(L);
-        mSorted = true;
+    /**
+     * Reset status to orignal status
+     */
+    public void reset() {
+        if (DEBUG) {
+            Log.d(TAG, "reset");
+        }
+        mUnfinishedStates.clear();
+        mFinishedStates.clear();
+        for (State state: mStates) {
+            state.mStatus = STATUS_ZERO;
+            state.mInvokedOutTransitions = 0;
+            if (state.mOutgoings != null) {
+                for (Transition t: state.mOutgoings) {
+                    t.mState = STATUS_ZERO;
+                }
+            }
+        }
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index effbc6e..2c9f069 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -1747,7 +1747,12 @@
     void slideIn() {
         if (mIsSlidingChildViews) {
             mIsSlidingChildViews = false;
-            scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+            if (mFocusPosition >= 0) {
+                scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+            } else {
+                mLayoutEatenInSliding = false;
+                requestLayout();
+            }
             if (mLayoutEatenInSliding) {
                 mLayoutEatenInSliding = false;
                 if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
@@ -1767,6 +1772,41 @@
         }
     }
 
+    int getSlideOutDistance() {
+        int distance;
+        if (mOrientation == VERTICAL) {
+            distance = -getHeight();
+            if (getChildCount() > 0) {
+                int top = getChildAt(0).getTop();
+                if (top < 0) {
+                    // scroll more if first child is above top edge
+                    distance = distance + top;
+                }
+            }
+        } else {
+            if (mReverseFlowPrimary) {
+                distance = getWidth();
+                if (getChildCount() > 0) {
+                    int start = getChildAt(0).getRight();
+                    if (start > distance) {
+                        // scroll more if first child is outside right edge
+                        distance = start;
+                    }
+                }
+            } else {
+                distance = -getWidth();
+                if (getChildCount() > 0) {
+                    int start = getChildAt(0).getLeft();
+                    if (start < 0) {
+                        // scroll more if first child is out side left edge
+                        distance = distance + start;
+                    }
+                }
+            }
+        }
+        return distance;
+    }
+
     /**
      * Temporarily slide out child and block layout and scroll requests.
      */
@@ -1779,31 +1819,11 @@
             return;
         }
         if (mOrientation == VERTICAL) {
-            int distance = -getHeight();
-            int top = getChildAt(0).getTop();
-            if (top < 0) {
-                // scroll more if first child is above top edge
-                distance = distance + top;
-            }
-            mBaseGridView.smoothScrollBy(0, distance, new AccelerateDecelerateInterpolator());
+            mBaseGridView.smoothScrollBy(0, getSlideOutDistance(),
+                    new AccelerateDecelerateInterpolator());
         } else {
-            int distance;
-            if (mReverseFlowPrimary) {
-                distance = getWidth();
-                int start = getChildAt(0).getRight();
-                if (start > distance) {
-                    // scroll more if first child is outside right edge
-                    distance = start;
-                }
-            } else {
-                distance = -getWidth();
-                int start = getChildAt(0).getLeft();
-                if (start < 0) {
-                    // scroll more if first child is out side left edge
-                    distance = distance + start;
-                }
-            }
-            mBaseGridView.smoothScrollBy(distance, 0, new AccelerateDecelerateInterpolator());
+            mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0,
+                    new AccelerateDecelerateInterpolator());
         }
     }
 
@@ -1969,8 +1989,12 @@
         }
 
         if (mIsSlidingChildViews) {
-            mLayoutEatenInSliding = true;
-            return;
+            // if there is already children, delay the layout process until slideIn(), if it's
+            // first time layout children: scroll them offscreen at end of onLayoutChildren()
+            if (getChildCount() > 0) {
+                mLayoutEatenInSliding = true;
+                return;
+            }
         }
         if (!mLayoutEnabled) {
             discardLayoutInfo();
@@ -2019,10 +2043,6 @@
 
         if (mInFastRelayout = layoutInit()) {
             fastRelayout();
-            // appends items till focus position.
-            if (mFocusPosition != NO_POSITION) {
-                scrollToFocusViewInLayout(hadFocus, scrollToFocus);
-            }
         } else {
             mInLayoutSearchFocus = hadFocus;
             if (mFocusPosition != NO_POSITION) {
@@ -2030,23 +2050,24 @@
                 while (appendOneColumnVisibleItems()
                         && findViewByPosition(mFocusPosition) == null) ;
             }
-            // multiple rounds: scrollToView of first round may drag first/last child into
-            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
-            int oldFirstVisible;
-            int oldLastVisible;
-            do {
-                updateScrollMin();
-                updateScrollMax();
-                oldFirstVisible = mGrid.getFirstVisibleIndex();
-                oldLastVisible = mGrid.getLastVisibleIndex();
-                scrollToFocusViewInLayout(hadFocus, true);
-                appendVisibleItems();
-                prependVisibleItems();
-                removeInvisibleViewsAtFront();
-                removeInvisibleViewsAtEnd();
-            } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
-                    || mGrid.getLastVisibleIndex() != oldLastVisible);
         }
+        // multiple rounds: scrollToView of first round may drag first/last child into
+        // "visible window" and we update scrollMin/scrollMax then run second scrollToView
+        // we must do this for fastRelayout() for the append item case
+        int oldFirstVisible;
+        int oldLastVisible;
+        do {
+            updateScrollMin();
+            updateScrollMax();
+            oldFirstVisible = mGrid.getFirstVisibleIndex();
+            oldLastVisible = mGrid.getLastVisibleIndex();
+            scrollToFocusViewInLayout(hadFocus, scrollToFocus);
+            appendVisibleItems();
+            prependVisibleItems();
+            removeInvisibleViewsAtFront();
+            removeInvisibleViewsAtEnd();
+        } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
+                || mGrid.getLastVisibleIndex() != oldLastVisible);
 
         if (scrollToFocus) {
             scrollDirectionPrimary(-delta);
@@ -2081,6 +2102,9 @@
         }
         dispatchChildSelectedAndPositioned();
 
+        if (mIsSlidingChildViews) {
+            scrollDirectionPrimary(getSlideOutDistance());
+        }
         mInLayout = false;
         leaveContext();
         if (DEBUG) Log.v(getTag(), "layoutChildren end");
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index c8db4bd..b92e518 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -15,9 +15,9 @@
  */
 package android.support.v17.leanback.app;
 
-import static junit.framework.TestCase.assertFalse;
-
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.animation.PropertyValuesHolder;
@@ -29,6 +29,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
@@ -37,6 +38,8 @@
 import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
 import android.support.v17.leanback.widget.DetailsParallax;
 import android.support.v17.leanback.widget.DetailsParallaxDrawable;
 import android.support.v17.leanback.widget.ParallaxTarget;
@@ -595,6 +598,13 @@
         );
 
         // after setup Video Playback the DPAD up will navigate to Video Fragment.
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+                @Override
+                    public boolean canProceed() {
+                        return detailsFragment.mVideoFragment != null
+                                && detailsFragment.mVideoFragment.getView() != null;
+                }
+        });
         sendKeys(KeyEvent.KEYCODE_DPAD_UP);
         assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
@@ -771,8 +781,6 @@
         assertTrue(recyclerViewHeight > 0);
 
         assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
-        assertEquals(255, detailsFragment.mDetailsBackgroundController.mParallaxDrawable
-                .getAlpha());
         Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
         assertEquals(0, coverDrawable.getBounds().top);
         assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
@@ -780,4 +788,320 @@
         assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
         assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
     }
+
+    public static class DetailsFragmentSwitchToVideoInOnCreate extends DetailsTestFragment {
+
+        final DetailsFragmentBackgroundController mDetailsBackground =
+                new DetailsFragmentBackgroundController(this);
+
+        public DetailsFragmentSwitchToVideoInOnCreate() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mDetailsBackground.switchToVideo();
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void switchToVideoInOnCreate() {
+        launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+                (DetailsFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+        // the pending enter transition flag should be automatically cleared
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+        assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+        assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+        //SystemClock.sleep(5000);
+        assertFalse(detailsFragment.isShowingTitle());
+
+        SystemClock.sleep(1000);
+        assertNull(detailsFragment.mVideoFragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+                        detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+                        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+                        glue.setArtist("A Googleer");
+                        glue.setTitle("Diving with Sharks");
+                        glue.setMediaSource(Uri.parse(
+                                "android.resource://android.support.v17.leanback.test/raw/video"));
+                    }
+                }
+        );
+        // once the video fragment is created it would be immediately assigned focus
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoFragment != null
+                        && detailsFragment.mVideoFragment.getView() != null
+                        && detailsFragment.mVideoFragment.getView().hasFocus();
+            }
+        });
+        // wait auto hide play controls done:
+        PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+            }
+        });
+
+        // switchToRows does nothing if there is no row
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        detailsFragment.mDetailsBackgroundController.switchToRows();
+                    }
+                }
+        );
+        assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+
+        // create item, it should be layout outside screen
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        detailsFragment.setItem(new PhotoItem("Hello world",
+                                "Fake content goes here",
+                                android.support.v17.leanback.test.R.drawable.spiderman));
+                    }
+                }
+        );
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.getVerticalGridView().getChildCount() > 0
+                        && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+                        >= detailsFragment.getVerticalGridView().getHeight();
+            }
+        });
+
+        // pressing BACK will return to details row
+        sendKeys(KeyEvent.KEYCODE_BACK);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+                        < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+            }
+        });
+        assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+    }
+
+    @Test
+    public void switchToVideoBackToQuit() {
+        launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+                (DetailsFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+        // the pending enter transition flag should be automatically cleared
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+        assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+        assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+        assertFalse(detailsFragment.isShowingTitle());
+
+        SystemClock.sleep(1000);
+        assertNull(detailsFragment.mVideoFragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+                        detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+                        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+                        glue.setArtist("A Googleer");
+                        glue.setTitle("Diving with Sharks");
+                        glue.setMediaSource(Uri.parse(
+                                "android.resource://android.support.v17.leanback.test/raw/video"));
+                    }
+                }
+        );
+        // once the video fragment is created it would be immediately assigned focus
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoFragment != null
+                        && detailsFragment.mVideoFragment.getView() != null
+                        && detailsFragment.mVideoFragment.getView().hasFocus();
+            }
+        });
+        // wait auto hide play controls done:
+        PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+            }
+        });
+
+        // before any details row is presented, pressing BACK will quit the activity
+        sendKeys(KeyEvent.KEYCODE_BACK);
+        PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(mActivity));
+    }
+
+    public static class DetailsFragmentSwitchToVideoAndPrepareEntranceTransition
+            extends DetailsTestFragment {
+
+        final DetailsFragmentBackgroundController mDetailsBackground =
+                new DetailsFragmentBackgroundController(this);
+
+        public DetailsFragmentSwitchToVideoAndPrepareEntranceTransition() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mDetailsBackground.switchToVideo();
+            prepareEntranceTransition();
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+        launchAndWaitActivity(DetailsFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+                (DetailsFragmentSwitchToVideoAndPrepareEntranceTransition)
+                        mActivity.getTestFragment();
+
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+    }
+
+    public static class DetailsFragmentEntranceTransition
+            extends DetailsTestFragment {
+
+        final DetailsFragmentBackgroundController mDetailsBackground =
+                new DetailsFragmentBackgroundController(this);
+
+        public DetailsFragmentEntranceTransition() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            prepareEntranceTransition();
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void entranceTransitionBlocksSwitchToVideo() {
+        launchAndWaitActivity(DetailsFragmentEntranceTransition.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsFragmentEntranceTransition detailsFragment =
+                (DetailsFragmentEntranceTransition)
+                        mActivity.getTestFragment();
+
+        if (Build.VERSION.SDK_INT < 21) {
+            // when enter transition is not supported, mCanUseHost is immmediately true
+            assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+        } else {
+            // calling switchToVideo() between prepareEntranceTransition and entrance transition
+            // finishes will be ignored.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    detailsFragment.mDetailsBackgroundController.switchToVideo();
+                }
+            });
+            assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+        }
+        assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+                        android.support.v17.leanback.test.R.drawable.spiderman));
+                detailsFragment.startEntranceTransition();
+            }
+        });
+        // once Entrance transition is finished, mCanUseHost will be true
+        // and we can switchToVideo and fade out the background.
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.mDetailsBackgroundController.switchToVideo();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+            }
+        });
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
index 03277bd..2b4a2d9 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -18,9 +18,9 @@
  */
 package android.support.v17.leanback.app;
 
-import static junit.framework.TestCase.assertFalse;
-
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.animation.PropertyValuesHolder;
@@ -32,6 +32,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
@@ -40,6 +41,8 @@
 import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
 import android.support.v17.leanback.widget.DetailsParallax;
 import android.support.v17.leanback.widget.DetailsParallaxDrawable;
 import android.support.v17.leanback.widget.ParallaxTarget;
@@ -598,6 +601,13 @@
         );
 
         // after setup Video Playback the DPAD up will navigate to Video Fragment.
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+                @Override
+                    public boolean canProceed() {
+                        return detailsFragment.mVideoSupportFragment != null
+                                && detailsFragment.mVideoSupportFragment.getView() != null;
+                }
+        });
         sendKeys(KeyEvent.KEYCODE_DPAD_UP);
         assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
@@ -774,8 +784,6 @@
         assertTrue(recyclerViewHeight > 0);
 
         assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
-        assertEquals(255, detailsFragment.mDetailsBackgroundController.mParallaxDrawable
-                .getAlpha());
         Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
         assertEquals(0, coverDrawable.getBounds().top);
         assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
@@ -783,4 +791,320 @@
         assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
         assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
     }
+
+    public static class DetailsSupportFragmentSwitchToVideoInOnCreate extends DetailsTestSupportFragment {
+
+        final DetailsSupportFragmentBackgroundController mDetailsBackground =
+                new DetailsSupportFragmentBackgroundController(this);
+
+        public DetailsSupportFragmentSwitchToVideoInOnCreate() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mDetailsBackground.switchToVideo();
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void switchToVideoInOnCreate() {
+        launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+                (DetailsSupportFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+        // the pending enter transition flag should be automatically cleared
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+        assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+        assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+        //SystemClock.sleep(5000);
+        assertFalse(detailsFragment.isShowingTitle());
+
+        SystemClock.sleep(1000);
+        assertNull(detailsFragment.mVideoSupportFragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+                        detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+                        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+                        glue.setArtist("A Googleer");
+                        glue.setTitle("Diving with Sharks");
+                        glue.setMediaSource(Uri.parse(
+                                "android.resource://android.support.v17.leanback.test/raw/video"));
+                    }
+                }
+        );
+        // once the video fragment is created it would be immediately assigned focus
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoSupportFragment != null
+                        && detailsFragment.mVideoSupportFragment.getView() != null
+                        && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+            }
+        });
+        // wait auto hide play controls done:
+        PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+            }
+        });
+
+        // switchToRows does nothing if there is no row
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        detailsFragment.mDetailsBackgroundController.switchToRows();
+                    }
+                }
+        );
+        assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+
+        // create item, it should be layout outside screen
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        detailsFragment.setItem(new PhotoItem("Hello world",
+                                "Fake content goes here",
+                                android.support.v17.leanback.test.R.drawable.spiderman));
+                    }
+                }
+        );
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.getVerticalGridView().getChildCount() > 0
+                        && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+                        >= detailsFragment.getVerticalGridView().getHeight();
+            }
+        });
+
+        // pressing BACK will return to details row
+        sendKeys(KeyEvent.KEYCODE_BACK);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+                        < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+            }
+        });
+        assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+    }
+
+    @Test
+    public void switchToVideoBackToQuit() {
+        launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+                (DetailsSupportFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+        // the pending enter transition flag should be automatically cleared
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+        assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+        assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+        assertFalse(detailsFragment.isShowingTitle());
+
+        SystemClock.sleep(1000);
+        assertNull(detailsFragment.mVideoSupportFragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+                        detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+                        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+                        glue.setArtist("A Googleer");
+                        glue.setTitle("Diving with Sharks");
+                        glue.setMediaSource(Uri.parse(
+                                "android.resource://android.support.v17.leanback.test/raw/video"));
+                    }
+                }
+        );
+        // once the video fragment is created it would be immediately assigned focus
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoSupportFragment != null
+                        && detailsFragment.mVideoSupportFragment.getView() != null
+                        && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+            }
+        });
+        // wait auto hide play controls done:
+        PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+            }
+        });
+
+        // before any details row is presented, pressing BACK will quit the activity
+        sendKeys(KeyEvent.KEYCODE_BACK);
+        PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(mActivity));
+    }
+
+    public static class DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition
+            extends DetailsTestSupportFragment {
+
+        final DetailsSupportFragmentBackgroundController mDetailsBackground =
+                new DetailsSupportFragmentBackgroundController(this);
+
+        public DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mDetailsBackground.switchToVideo();
+            prepareEntranceTransition();
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+        launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+                (DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition)
+                        mActivity.getTestFragment();
+
+        assertEquals(StateMachine.STATUS_INVOKED,
+                detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+    }
+
+    public static class DetailsSupportFragmentEntranceTransition
+            extends DetailsTestSupportFragment {
+
+        final DetailsSupportFragmentBackgroundController mDetailsBackground =
+                new DetailsSupportFragmentBackgroundController(this);
+
+        public DetailsSupportFragmentEntranceTransition() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            prepareEntranceTransition();
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            mDetailsBackground.setCoverBitmap(bitmap);
+        }
+
+        @Override
+        public void onStop() {
+            mDetailsBackground.setCoverBitmap(null);
+            super.onStop();
+        }
+    }
+
+    @Test
+    public void entranceTransitionBlocksSwitchToVideo() {
+        launchAndWaitActivity(DetailsSupportFragmentEntranceTransition.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsSupportFragmentEntranceTransition detailsFragment =
+                (DetailsSupportFragmentEntranceTransition)
+                        mActivity.getTestFragment();
+
+        if (Build.VERSION.SDK_INT < 21) {
+            // when enter transition is not supported, mCanUseHost is immmediately true
+            assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+        } else {
+            // calling switchToVideo() between prepareEntranceTransition and entrance transition
+            // finishes will be ignored.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    detailsFragment.mDetailsBackgroundController.switchToVideo();
+                }
+            });
+            assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+        }
+        assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+                        android.support.v17.leanback.test.R.drawable.spiderman));
+                detailsFragment.startEntranceTransition();
+            }
+        });
+        // once Entrance transition is finished, mCanUseHost will be true
+        // and we can switchToVideo and fade out the background.
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.mDetailsBackgroundController.switchToVideo();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+            }
+        });
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
index 193203e..5f2fc81 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Rect;
@@ -29,8 +30,13 @@
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.View;
@@ -232,4 +238,57 @@
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
 
     }
+
+    public static class F_ListRowWithOnClick extends RowsFragment {
+        Presenter.ViewHolder mLastClickedItemViewHolder;
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setOnItemViewClickedListener(new OnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                        RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    mLastClickedItemViewHolder = itemViewHolder;
+                }
+            });
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void prefetchChildItemsBeforeAttach() throws Throwable {
+        launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) mActivity.getTestFragment();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        gridView.setSelectedPositionSmooth(lastRowPos);
+                    }
+                }
+        );
+        waitForScrollIdle(gridView);
+        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+        fragment.mLastClickedItemViewHolder = null;
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+                    }
+                }
+        );
+        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
index 70ddbac..8ebb047 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Rect;
@@ -32,8 +33,13 @@
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.View;
@@ -235,4 +241,57 @@
         assertNotNull(gridView.findViewHolderForAdapterPosition(7));
 
     }
+
+    public static class F_ListRowWithOnClick extends RowsSupportFragment {
+        Presenter.ViewHolder mLastClickedItemViewHolder;
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setOnItemViewClickedListener(new OnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                        RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    mLastClickedItemViewHolder = itemViewHolder;
+                }
+            });
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void prefetchChildItemsBeforeAttach() throws Throwable {
+        launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) mActivity.getTestFragment();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        gridView.setSelectedPositionSmooth(lastRowPos);
+                    }
+                }
+        );
+        waitForScrollIdle(gridView);
+        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+        fragment.mLastClickedItemViewHolder = null;
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+                    }
+                }
+        );
+        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
index d17811b..6047a1e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.util.Log;
 
 public class SingleFragmentTestActivity extends Activity {
 
@@ -32,10 +33,12 @@
     public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
 
     public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+    private static final String TAG = "TestActivity";
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        Log.d(TAG, "onCreate " + this);
         Intent intent = getIntent();
 
         final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
index 6ecd050..ad4881e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -21,6 +21,7 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 
 import org.junit.After;
 import org.junit.Rule;
@@ -29,6 +30,7 @@
 public class SingleFragmentTestBase {
 
     private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+    private static final String TAG = "SingleFragmentTestBase";
 
     @Rule
     public TestName mUnitTestName = new TestName();
@@ -43,6 +45,7 @@
     public void afterTest() throws Throwable {
         final SingleFragmentTestActivity activity = mActivity;
         if (activity != null) {
+            Log.d(TAG, "wait finish " + activity + " for " + this);
             mActivity = null;
             activityTestRule.runOnUiThread(new Runnable() {
                 public void run() {
@@ -105,6 +108,7 @@
             options.collect(intent);
         }
         mActivity = activityTestRule.launchActivity(intent);
+        Log.d(TAG, "launched " + mActivity + " for " + this, new Exception());
         SystemClock.sleep(waitTimeMs);
     }
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
index 911a32e..0fc3183 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -24,6 +24,7 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.util.Log;
 
 public class SingleSupportFragmentTestActivity extends FragmentActivity {
 
@@ -35,10 +36,12 @@
     public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
 
     public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+    private static final String TAG = "TestActivity";
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        Log.d(TAG, "onCreate " + this);
         Intent intent = getIntent();
 
         final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
index dc3d97f..82d69d0 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
@@ -24,6 +24,7 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 
 import org.junit.After;
 import org.junit.Rule;
@@ -32,6 +33,7 @@
 public class SingleSupportFragmentTestBase {
 
     private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+    private static final String TAG = "SingleSupportFragmentTestBase";
 
     @Rule
     public TestName mUnitTestName = new TestName();
@@ -46,6 +48,7 @@
     public void afterTest() throws Throwable {
         final SingleSupportFragmentTestActivity activity = mActivity;
         if (activity != null) {
+            Log.d(TAG, "wait finish " + activity + " for " + this);
             mActivity = null;
             activityTestRule.runOnUiThread(new Runnable() {
                 public void run() {
@@ -108,6 +111,7 @@
             options.collect(intent);
         }
         mActivity = activityTestRule.launchActivity(intent);
+        Log.d(TAG, "launched " + mActivity + " for " + this, new Exception());
         SystemClock.sleep(waitTimeMs);
     }
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
new file mode 100644
index 0000000..a041c53
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.media;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.test.InstrumentationRegistry;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class PlaybackControlGlueTest {
+
+    public static class PlaybackControlGlueImpl extends PlaybackControlGlue {
+
+        public PlaybackControlGlueImpl(Context context) {
+            super(context, new int[] {PLAYBACK_SPEED_FAST_L0, PLAYBACK_SPEED_FAST_L1});
+        }
+
+        @Override
+        public boolean hasValidMedia() {
+            return true;
+        }
+
+        @Override
+        public boolean isMediaPlaying() {
+            return false;
+        }
+
+        @Override
+        public CharSequence getMediaTitle() {
+            return null;
+        }
+
+        @Override
+        public CharSequence getMediaSubtitle() {
+            return null;
+        }
+
+        @Override
+        public int getMediaDuration() {
+            return 0;
+        }
+
+        @Override
+        public Drawable getMediaArt() {
+            return null;
+        }
+
+        @Override
+        public long getSupportedActions() {
+            return 0;
+        }
+
+        @Override
+        public int getCurrentSpeedId() {
+            return 0;
+        }
+
+        @Override
+        public int getCurrentPosition() {
+            return 0;
+        }
+    }
+
+    Context mContext;
+    PlaybackControlGlue mGlue;
+
+    @Test
+    public void usingDefaultRowAndPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+            }
+        });
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertTrue(host.mPlaybackRowPresenter instanceof PlaybackControlsRowPresenter);
+        assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+    }
+    @Test
+    public void customRowPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+            }
+        });
+        PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+            @Override
+            protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+                return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+            }
+        };
+        mGlue.setPlaybackRowPresenter(presenter);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertSame(host.mPlaybackRowPresenter, presenter);
+        assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+    }
+
+    @Test
+    public void customControlsRow() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+            }
+        });
+        PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+        mGlue.setControlsRow(row);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertTrue(host.mPlaybackRowPresenter instanceof PlaybackControlsRowPresenter);
+        assertSame(host.mRow, row);
+
+    }
+
+    @Test
+    public void customRowAndPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+            }
+        });
+        PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+        mGlue.setControlsRow(row);
+        PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+            @Override
+            protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+                return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+            }
+        };
+        mGlue.setPlaybackRowPresenter(presenter);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertSame(host.mPlaybackRowPresenter, presenter);
+        assertSame(host.mRow, row);
+
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
index 199ab3e..2c9aa43 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -15,12 +15,17 @@
  */
 package android.support.v17.leanback.media;
 
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.Row;
+
 /**
  * Fake PlaybackGlueHost used by test.
  */
 public class PlaybackGlueHostImpl extends PlaybackGlueHost {
 
     HostCallback mHostCallback;
+    Row mRow;
+    PlaybackRowPresenter mPlaybackRowPresenter;
 
     @Override
     public void setHostCallback(HostCallback callback) {
@@ -56,4 +61,14 @@
             mHostCallback.onHostDestroy();
         }
     }
+
+    @Override
+    public void setPlaybackRow(Row row) {
+        mRow = row;
+    }
+
+    @Override
+    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+        mPlaybackRowPresenter = presenter;
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 8bbe406..0195ef7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -785,6 +785,73 @@
     }
 
     @Test
+    public void testAddLastItemHorizontal() throws Throwable {
+
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mGridView.setSelectedPositionSmooth(49);
+                    }
+                }
+        );
+        waitForScrollIdle(mVerifyLayout);
+        performAndWaitForAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.addItems(50, new int[]{150});
+            }
+        });
+
+        // assert new added item aligned to right edge
+        assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+                mGridView.getLayoutManager().findViewByPosition(50).getRight());
+    }
+
+    @Test
+    public void testAddMultipleLastItemsHorizontal() throws Throwable {
+
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_BOTH_EDGE);
+                        mGridView.setWindowAlignmentOffsetPercent(50);
+                        mGridView.setSelectedPositionSmooth(49);
+                    }
+                }
+        );
+        waitForScrollIdle(mVerifyLayout);
+        performAndWaitForAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.addItems(50, new int[]{150, 150, 150, 150, 150, 150, 150, 150, 150,
+                        150, 150, 150, 150, 150});
+            }
+        });
+
+        // The focused item will be at center of window
+        View view = mGridView.getLayoutManager().findViewByPosition(49);
+        assertEquals(mGridView.getWidth() / 2, (view.getLeft() + view.getRight()) / 2);
+    }
+
+    @Test
     public void testItemAddRemoveHorizontal() throws Throwable {
 
         Intent intent = new Intent();
diff --git a/v7/appcompat/res-public/values/public_attrs.xml b/v7/appcompat/res-public/values/public_attrs.xml
index 2e16fe7..a7829eb 100644
--- a/v7/appcompat/res-public/values/public_attrs.xml
+++ b/v7/appcompat/res-public/values/public_attrs.xml
@@ -195,6 +195,8 @@
      <public type="attr" name="tickMark"/>
      <public type="attr" name="tickMarkTint"/>
      <public type="attr" name="tickMarkTintMode"/>
+     <public type="attr" name="tint"/>
+     <public type="attr" name="tintMode"/>
      <public type="attr" name="title"/>
      <public type="attr" name="titleMargin"/>
      <public type="attr" name="titleMarginBottom"/>
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 9fc767a..d5ec5a1 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -1063,6 +1063,27 @@
         <!-- Sets a drawable as the content of this ImageView. Allows the use of vector drawable
              when running on older versions of the platform. -->
         <attr name="srcCompat" format="reference" />
+
+        <!-- Tint to apply to the image source. -->
+        <attr name="tint" format="color" />
+
+        <!-- Blending mode used to apply the image source tint. -->
+        <attr name="tintMode">
+            <!-- The tint is drawn on top of the drawable.
+                 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] -->
+            <enum name="src_over" value="3" />
+            <!-- The tint is masked by the alpha channel of the drawable. The drawable’s
+                 color channels are thrown out. [Sa * Da, Sc * Da] -->
+            <enum name="src_in" value="5" />
+            <!-- The tint is drawn above the drawable, but with the drawable’s alpha
+                 channel masking the result. [Da, Sc * Da + (1 - Sa) * Dc] -->
+            <enum name="src_atop" value="9" />
+            <!-- Multiplies the color and alpha channels of the drawable with those of
+                 the tint. [Sa * Da, Sc * Dc] -->
+            <enum name="multiply" value="14" />
+            <!-- [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] -->
+            <enum name="screen" value="15" />
+        </attr>
     </declare-styleable>
 
     <declare-styleable name="AppCompatSeekBar">
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
index edbd1a7..96a481e 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
@@ -37,7 +37,7 @@
  * <ul>
  *     <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
  *     {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
@@ -53,8 +53,8 @@
             android.R.attr.popupBackground
     };
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatTextHelper mTextHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatTextHelper mTextHelper;
 
     public AppCompatAutoCompleteTextView(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java b/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
index a4551db..cb0ef68 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
@@ -148,18 +148,18 @@
 
     private boolean shouldApplyFrameworkTintUsingColorFilter() {
         final int sdk = Build.VERSION.SDK_INT;
-        if (sdk < 21) {
-            // API 19 and below doesn't have framework tint
-            return false;
+        if (sdk > 21) {
+            // On API 22+, if we're using an internal compat background tint, we're also
+            // responsible for applying any custom tint set via the framework impl
+            return mInternalBackgroundTint != null;
         } else if (sdk == 21) {
             // GradientDrawable doesn't implement setTintList on API 21, and since there is
             // no nice way to unwrap DrawableContainers we have to blanket apply this
             // on API 21
             return true;
         } else {
-            // On API 22+, if we're using an internal compat background tint, we're also
-            // responsible for applying any custom tint set via the framework impl
-            return mInternalBackgroundTint != null;
+            // API 19 and below doesn't have framework tint
+            return false;
         }
     }
 
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
index f7fa23f..c2075b9 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
@@ -40,7 +40,7 @@
  * <ul>
  *     <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
  *     {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
index 6942cc5..5809d25 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
@@ -35,7 +35,7 @@
  * A {@link CheckBox} which supports compatible features on older version of the platform,
  * including:
  * <ul>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.widget.CompoundButtonCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
  *     {@link R.attr#buttonTintMode}.</li>
@@ -46,7 +46,7 @@
  */
 public class AppCompatCheckBox extends CheckBox implements TintableCompoundButton {
 
-    private AppCompatCompoundButtonHelper mCompoundButtonHelper;
+    private final AppCompatCompoundButtonHelper mCompoundButtonHelper;
 
     public AppCompatCheckBox(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
index fadf328..1725c24 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
@@ -34,7 +34,7 @@
             android.R.attr.checkMark
     };
 
-    private AppCompatTextHelper mTextHelper;
+    private final AppCompatTextHelper mTextHelper;
 
     public AppCompatCheckedTextView(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java b/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
index 9ea02c6..d93f399 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
@@ -36,7 +36,7 @@
  * <ul>
  *     <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
  *     {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
@@ -47,8 +47,8 @@
  */
 public class AppCompatEditText extends EditText implements TintableBackgroundView {
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatTextHelper mTextHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatTextHelper mTextHelper;
 
     public AppCompatEditText(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
index f8280c9..d0bcf00 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
@@ -20,33 +20,44 @@
 
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 import android.support.v4.view.TintableBackgroundView;
+import android.support.v4.widget.ImageViewCompat;
+import android.support.v4.widget.TintableImageSourceView;
 import android.support.v7.appcompat.R;
 import android.util.AttributeSet;
 import android.widget.ImageButton;
+import android.widget.ImageView;
 
 /**
  * A {@link ImageButton} which supports compatible features on older version of the platform,
  * including:
  * <ul>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
+ *     <li>Allows dynamic tint of its image via the image tint methods in
+ *     {@link ImageViewCompat}.</li>
+ *     <li>Allows setting of the image tint using {@link R.attr#tint} and
+ *     {@link R.attr#tintMode}.</li>
  * </ul>
  *
  * <p>This will automatically be used when you use {@link android.widget.ImageButton} in your
  * layouts. You should only need to manually use this class when writing custom views.</p>
  */
-public class AppCompatImageButton extends ImageButton implements TintableBackgroundView {
+public class AppCompatImageButton extends ImageButton implements TintableBackgroundView,
+        TintableImageSourceView {
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatImageHelper mImageHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatImageHelper mImageHelper;
 
     public AppCompatImageButton(Context context) {
         this(context, null);
@@ -73,6 +84,38 @@
     }
 
     @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        super.setImageDrawable(drawable);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageBitmap(Bitmap bm) {
+        super.setImageBitmap(bm);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageIcon(@Nullable Icon icon) {
+        super.setImageIcon(icon);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageURI(@Nullable Uri uri) {
+        super.setImageURI(uri);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
     public void setBackgroundResource(@DrawableRes int resId) {
         super.setBackgroundResource(resId);
         if (mBackgroundTintHelper != null) {
@@ -143,6 +186,61 @@
         return mBackgroundTintHelper != null
                 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
     }
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#setImageTintList(ImageView, ColorStateList)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setSupportImageTintList(@Nullable ColorStateList tint) {
+        if (mImageHelper != null) {
+            mImageHelper.setSupportImageTintList(tint);
+        }
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#getImageTintList(ImageView)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public ColorStateList getSupportImageTintList() {
+        return mImageHelper != null
+                ? mImageHelper.getSupportImageTintList() : null;
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#setImageTintMode(ImageView, PorterDuff.Mode)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mImageHelper != null) {
+            mImageHelper.setSupportImageTintMode(tintMode);
+        }
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#getImageTintMode(ImageView)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public PorterDuff.Mode getSupportImageTintMode() {
+        return mImageHelper != null
+                ? mImageHelper.getSupportImageTintMode() : null;
+    }
 
     @Override
     protected void drawableStateChanged() {
@@ -150,6 +248,9 @@
         if (mBackgroundTintHelper != null) {
             mBackgroundTintHelper.applySupportBackgroundTint();
         }
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
     }
 
     public boolean hasOverlappingRendering() {
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
index fe733f7..12b8520 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
@@ -18,9 +18,13 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
+import android.support.v4.widget.ImageViewCompat;
 import android.support.v7.appcompat.R;
 import android.support.v7.content.res.AppCompatResources;
 import android.util.AttributeSet;
@@ -31,22 +35,22 @@
  */
 @RestrictTo(LIBRARY_GROUP)
 public class AppCompatImageHelper {
-
     private final ImageView mView;
 
+    private TintInfo mInternalImageTint;
+    private TintInfo mImageTint;
+    private TintInfo mTmpInfo;
+
     public AppCompatImageHelper(ImageView view) {
         mView = view;
     }
 
     public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
-        TintTypedArray a = null;
+        TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
+                R.styleable.AppCompatImageView, defStyleAttr, 0);
         try {
             Drawable drawable = mView.getDrawable();
-
             if (drawable == null) {
-                a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
-                        R.styleable.AppCompatImageView, defStyleAttr, 0);
-
                 // If the view doesn't already have a drawable (from android:src), try loading
                 // it from srcCompat
                 final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1);
@@ -61,10 +65,18 @@
             if (drawable != null) {
                 DrawableUtils.fixDrawable(drawable);
             }
-        } finally {
-            if (a != null) {
-                a.recycle();
+
+            if (a.hasValue(R.styleable.AppCompatImageView_tint)) {
+                ImageViewCompat.setImageTintList(mView,
+                        a.getColorStateList(R.styleable.AppCompatImageView_tint));
             }
+            if (a.hasValue(R.styleable.AppCompatImageView_tintMode)) {
+                ImageViewCompat.setImageTintMode(mView,
+                        DrawableUtils.parseTintMode(
+                                a.getInt(R.styleable.AppCompatImageView_tintMode, -1), null));
+            }
+        } finally {
+            a.recycle();
         }
     }
 
@@ -78,6 +90,8 @@
         } else {
             mView.setImageDrawable(null);
         }
+
+        applySupportImageTint();
     }
 
     boolean hasOverlappingRendering() {
@@ -90,4 +104,116 @@
         }
         return true;
     }
+
+    void setSupportImageTintList(ColorStateList tint) {
+        if (mImageTint == null) {
+            mImageTint = new TintInfo();
+        }
+        mImageTint.mTintList = tint;
+        mImageTint.mHasTintList = true;
+        applySupportImageTint();
+    }
+
+    ColorStateList getSupportImageTintList() {
+        return mImageTint != null ? mImageTint.mTintList : null;
+    }
+
+    void setSupportImageTintMode(PorterDuff.Mode tintMode) {
+        if (mImageTint == null) {
+            mImageTint = new TintInfo();
+        }
+        mImageTint.mTintMode = tintMode;
+        mImageTint.mHasTintMode = true;
+
+        applySupportImageTint();
+    }
+
+    PorterDuff.Mode getSupportImageTintMode() {
+        return mImageTint != null ? mImageTint.mTintMode : null;
+    }
+
+    void applySupportImageTint() {
+        final Drawable imageViewDrawable = mView.getDrawable();
+        if (imageViewDrawable != null) {
+            DrawableUtils.fixDrawable(imageViewDrawable);
+        }
+
+        if (imageViewDrawable != null) {
+            if (shouldApplyFrameworkTintUsingColorFilter()
+                    && applyFrameworkTintUsingColorFilter(imageViewDrawable)) {
+                // This needs to be called before the internal tints below so it takes
+                // effect on any widgets using the compat tint on API 21
+                return;
+            }
+
+            if (mImageTint != null) {
+                AppCompatDrawableManager.tintDrawable(imageViewDrawable, mImageTint,
+                        mView.getDrawableState());
+            } else if (mInternalImageTint != null) {
+                AppCompatDrawableManager.tintDrawable(imageViewDrawable, mInternalImageTint,
+                        mView.getDrawableState());
+            }
+        }
+    }
+
+    void setInternalImageTint(ColorStateList tint) {
+        if (tint != null) {
+            if (mInternalImageTint == null) {
+                mInternalImageTint = new TintInfo();
+            }
+            mInternalImageTint.mTintList = tint;
+            mInternalImageTint.mHasTintList = true;
+        } else {
+            mInternalImageTint = null;
+        }
+        applySupportImageTint();
+    }
+
+    private boolean shouldApplyFrameworkTintUsingColorFilter() {
+        final int sdk = Build.VERSION.SDK_INT;
+        if (sdk > 21) {
+            // On API 22+, if we're using an internal compat image source tint, we're also
+            // responsible for applying any custom tint set via the framework impl
+            return mInternalImageTint != null;
+        } else if (sdk == 21) {
+            // GradientDrawable doesn't implement setTintList on API 21, and since there is
+            // no nice way to unwrap DrawableContainers we have to blanket apply this
+            // on API 21
+            return true;
+        } else {
+            // API 19 and below doesn't have framework tint
+            return false;
+        }
+    }
+
+    /**
+     * Applies the framework image source tint to a view, but using the compat method (ColorFilter)
+     *
+     * @return true if a tint was applied
+     */
+    private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable imageSource) {
+        if (mTmpInfo == null) {
+            mTmpInfo = new TintInfo();
+        }
+        final TintInfo info = mTmpInfo;
+        info.clear();
+
+        final ColorStateList tintList = ImageViewCompat.getImageTintList(mView);
+        if (tintList != null) {
+            info.mHasTintList = true;
+            info.mTintList = tintList;
+        }
+        final PorterDuff.Mode mode = ImageViewCompat.getImageTintMode(mView);
+        if (mode != null) {
+            info.mHasTintMode = true;
+            info.mTintMode = mode;
+        }
+
+        if (info.mHasTintList || info.mHasTintMode) {
+            AppCompatDrawableManager.tintDrawable(imageSource, info, mView.getDrawableState());
+            return true;
+        }
+
+        return false;
+    }
 }
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
index b749d6c..532a18b 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
@@ -20,12 +20,17 @@
 
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 import android.support.v4.view.TintableBackgroundView;
+import android.support.v4.widget.ImageViewCompat;
+import android.support.v4.widget.TintableImageSourceView;
 import android.support.v7.appcompat.R;
 import android.util.AttributeSet;
 import android.widget.ImageView;
@@ -34,19 +39,24 @@
  * A {@link ImageView} which supports compatible features on older version of the platform,
  * including:
  * <ul>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
+ *     <li>Allows dynamic tint of its image via the image tint methods in
+ *     {@link ImageViewCompat}.</li>
+ *     <li>Allows setting of the image tint using {@link R.attr#tint} and
+ *     {@link R.attr#tintMode}.</li>
  * </ul>
  *
  * <p>This will automatically be used when you use {@link android.widget.ImageView} in your
  * layouts. You should only need to manually use this class when writing custom views.</p>
  */
-public class AppCompatImageView extends ImageView implements TintableBackgroundView {
+public class AppCompatImageView extends ImageView implements TintableBackgroundView,
+        TintableImageSourceView {
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatImageHelper mImageHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatImageHelper mImageHelper;
 
     public AppCompatImageView(Context context) {
         this(context, null);
@@ -77,8 +87,42 @@
      */
     @Override
     public void setImageResource(@DrawableRes int resId) {
-        // Intercept this call and instead retrieve the Drawable via the image helper
-        mImageHelper.setImageResource(resId);
+        if (mImageHelper != null) {
+            // Intercept this call and instead retrieve the Drawable via the image helper
+            mImageHelper.setImageResource(resId);
+        }
+    }
+
+    @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        super.setImageDrawable(drawable);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageBitmap(Bitmap bm) {
+        super.setImageBitmap(bm);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageIcon(@Nullable Icon icon) {
+        super.setImageIcon(icon);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
+    }
+
+    @Override
+    public void setImageURI(@Nullable Uri uri) {
+        super.setImageURI(uri);
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
     }
 
     @Override
@@ -153,12 +197,71 @@
                 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
     }
 
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#setImageTintList(ImageView, ColorStateList)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setSupportImageTintList(@Nullable ColorStateList tint) {
+        if (mImageHelper != null) {
+            mImageHelper.setSupportImageTintList(tint);
+        }
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#getImageTintList(ImageView)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public ColorStateList getSupportImageTintList() {
+        return mImageHelper != null
+                ? mImageHelper.getSupportImageTintList() : null;
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#setImageTintMode(ImageView, PorterDuff.Mode)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mImageHelper != null) {
+            mImageHelper.setSupportImageTintMode(tintMode);
+        }
+    }
+
+    /**
+     * This should be accessed via
+     * {@link android.support.v4.widget.ImageViewCompat#getImageTintMode(ImageView)}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public PorterDuff.Mode getSupportImageTintMode() {
+        return mImageHelper != null
+                ? mImageHelper.getSupportImageTintMode() : null;
+    }
+
     @Override
     protected void drawableStateChanged() {
         super.drawableStateChanged();
         if (mBackgroundTintHelper != null) {
             mBackgroundTintHelper.applySupportBackgroundTint();
         }
+        if (mImageHelper != null) {
+            mImageHelper.applySupportImageTint();
+        }
     }
 
     public boolean hasOverlappingRendering() {
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
index 381169d..8060d7d 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
@@ -37,7 +37,7 @@
  * <ul>
  *     <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
  *     {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
@@ -53,8 +53,8 @@
             android.R.attr.popupBackground
     };
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatTextHelper mTextHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatTextHelper mTextHelper;
 
     public AppCompatMultiAutoCompleteTextView(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
index 768d610..8aa22f3 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
@@ -35,7 +35,7 @@
  * A {@link RadioButton} which supports compatible features on older version of the platform,
  * including:
  * <ul>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.widget.CompoundButtonCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
  *     {@link R.attr#buttonTintMode}.</li>
@@ -46,7 +46,7 @@
  */
 public class AppCompatRadioButton extends RadioButton implements TintableCompoundButton {
 
-    private AppCompatCompoundButtonHelper mCompoundButtonHelper;
+    private final AppCompatCompoundButtonHelper mCompoundButtonHelper;
 
     public AppCompatRadioButton(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java b/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
index afaa02a..762e97a 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
@@ -31,7 +31,7 @@
  */
 public class AppCompatRatingBar extends RatingBar {
 
-    private AppCompatProgressBarHelper mAppCompatProgressBarHelper;
+    private final AppCompatProgressBarHelper mAppCompatProgressBarHelper;
 
     public AppCompatRatingBar(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java b/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
index bac1cb8..8b476b8 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
@@ -32,7 +32,7 @@
  */
 public class AppCompatSeekBar extends SeekBar {
 
-    private AppCompatSeekBarHelper mAppCompatSeekBarHelper;
+    private final AppCompatSeekBarHelper mAppCompatSeekBarHelper;
 
     public AppCompatSeekBar(Context context) {
         this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java b/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
index 9f27f78..1be8ed8 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
@@ -55,11 +55,11 @@
  * A {@link Spinner} which supports compatible features on older versions of the platform,
  * including:
  * <ul>
- * <li>Dynamic tinting of the background via the background tint methods in
- * {@link android.support.v4.view.ViewCompat}.</li>
- * <li>Configuring the background tint using {@link R.attr#backgroundTint} and
- * {@link R.attr#backgroundTintMode}.</li>
- * <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
+ *     <li>Allows dynamic tint of its background via the background tint methods in
+ *     {@link android.support.v4.widget.CompoundButtonCompat}.</li>
+ *     <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
+ *     {@link R.attr#buttonTintMode}.</li>
+ *     <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
  * </ul>
  *
  * <p>This will automatically be used when you use {@link Spinner} in your layouts.
@@ -77,10 +77,10 @@
     private static final int MODE_DROPDOWN = 1;
     private static final int MODE_THEME = -1;
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
 
     /** Context used to inflate the popup window or dialog. */
-    private Context mPopupContext;
+    private final Context mPopupContext;
 
     /** Forwarding listener used to implement drag-to-open. */
     private ForwardingListener mForwardingListener;
@@ -88,13 +88,13 @@
     /** Temporary holder for setAdapter() calls from the super constructor. */
     private SpinnerAdapter mTempAdapter;
 
-    private boolean mPopupSet;
+    private final boolean mPopupSet;
 
-    DropdownPopup mPopup;
+    private DropdownPopup mPopup;
 
-    int mDropDownWidth;
+    private int mDropDownWidth;
 
-    final Rect mTempRect = new Rect();
+    private final Rect mTempRect = new Rect();
 
     /**
      * Construct a new spinner with the given context's theme.
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
index 66fd929..96dfd5a 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
@@ -36,7 +36,7 @@
  * <ul>
  *     <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
  *     {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- *     <li>Allows dynamic tint of it background via the background tint methods in
+ *     <li>Allows dynamic tint of its background via the background tint methods in
  *     {@link android.support.v4.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
@@ -47,8 +47,8 @@
  */
 public class AppCompatTextView extends TextView implements TintableBackgroundView {
 
-    private AppCompatBackgroundHelper mBackgroundTintHelper;
-    private AppCompatTextHelper mTextHelper;
+    private final AppCompatBackgroundHelper mBackgroundTintHelper;
+    private final AppCompatTextHelper mTextHelper;
 
     public AppCompatTextView(Context context) {
         this(context, null);
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index 7385a5f..14413a2 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -75,6 +75,11 @@
                 android:theme="@style/Theme.TextColors" />
 
         <activity
+                android:name="android.support.v7.widget.AppCompatImageButtonActivity"
+                android:label="@string/app_compat_image_button_activity"
+                android:theme="@style/Theme.AppCompat.Light" />
+
+        <activity
                 android:name="android.support.v7.widget.AppCompatImageViewActivity"
                 android:label="@string/app_compat_image_view_activity"
                 android:theme="@style/Theme.AppCompat.Light" />
diff --git a/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml b/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml
new file mode 100755
index 0000000..4be0ee0
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_tinted_no_background"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@null"
+            android:src="@drawable/test_drawable_blue"
+            app:backgroundTint="@color/color_state_list_lilac"
+            app:backgroundTintMode="src_in"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_tinted_background"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@drawable/test_drawable"
+            android:src="@drawable/test_drawable_blue"
+            app:backgroundTint="@color/color_state_list_lilac"
+            app:backgroundTintMode="src_in"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_untinted_no_background"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@null"
+            android:src="@drawable/test_drawable_blue"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_untinted_background"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@drawable/test_background_green"
+            android:src="@drawable/test_drawable_blue"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_tinted_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/test_drawable_blue"
+            app:tint="@color/color_state_list_lilac"
+            app:tintMode="src_in"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_tinted_no_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:tint="@color/color_state_list_lilac"
+            app:tintMode="src_in"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_untinted_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/test_drawable_blue"/>
+
+        <android.support.v7.widget.AppCompatImageButton
+            android:id="@+id/view_untinted_no_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+</ScrollView>
diff --git a/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml b/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
index c2aa5ed..b8af1b2 100644
--- a/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
+++ b/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
@@ -56,11 +56,44 @@
             android:background="@drawable/test_background_green" />
 
         <android.support.v7.widget.AppCompatImageView
-                android:id="@+id/view_android_src_srccompat"
-                android:layout_width="30dp"
-                android:layout_height="30dp"
-                android:src="@drawable/test_drawable_blue"
-                app:srcCompat="@drawable/test_drawable_red" />
+            android:id="@+id/view_android_src_srccompat"
+            android:layout_width="30dp"
+            android:layout_height="30dp"
+            android:src="@drawable/test_drawable_blue"
+            app:srcCompat="@drawable/test_drawable_red" />
+
+        <android.support.v7.widget.AppCompatImageView
+            android:id="@+id/view_tinted_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/test_drawable_blue"
+            app:tint="@color/color_state_list_lilac"
+            app:tintMode="src_in" />
+
+        <android.support.v7.widget.AppCompatImageView
+            android:id="@+id/view_tinted_no_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:tint="@color/color_state_list_lilac"
+            app:tintMode="src_in" />
+
+        <android.support.v7.widget.AppCompatImageView
+            android:id="@+id/view_untinted_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/test_drawable_blue" />
+
+        <android.support.v7.widget.AppCompatImageView
+            android:id="@+id/view_untinted_no_source"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <android.support.v7.widget.AppCompatImageView
+            android:id="@+id/view_tinted_android_src_srccompat"
+            android:layout_width="30dp"
+            android:layout_height="30dp"
+            android:src="@drawable/test_drawable_blue"
+            app:srcCompat="@drawable/test_drawable_red" />
 
     </LinearLayout>
 
diff --git a/v7/appcompat/tests/res/values/strings.xml b/v7/appcompat/tests/res/values/strings.xml
index 812bd61..926418a 100644
--- a/v7/appcompat/tests/res/values/strings.xml
+++ b/v7/appcompat/tests/res/values/strings.xml
@@ -53,6 +53,7 @@
     <string name="app_compat_text_view_activity">AppCompat text view</string>
     <string name="sample_text1">Sample text 1</string>
     <string name="sample_text2">Sample text 2</string>
+    <string name="app_compat_image_button_activity">AppCompat image button</string>
     <string name="app_compat_image_view_activity">AppCompat image view</string>
     <string name="app_compat_button_activity">AppCompat button</string>
     <string-array name="planets_array">
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java b/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
index de36207..357de5c 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
@@ -16,26 +16,28 @@
 
 package android.support.v7.testutils;
 
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+
+import static org.hamcrest.core.AllOf.allOf;
+import static org.hamcrest.core.AnyOf.anyOf;
+
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.DrawableRes;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
-import android.support.v4.view.TintableBackgroundView;
 import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.AppCompatTextView;
+import android.support.v4.widget.ImageViewCompat;
 import android.view.View;
-import org.hamcrest.Matcher;
+import android.widget.ImageView;
 
-import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
-import static org.hamcrest.core.AllOf.allOf;
+import org.hamcrest.Matcher;
 
 public class AppCompatTintableViewActions {
     /**
-     * Sets the passed color state list as the background layer on a {@link View}.
+     * Sets the passed color state list as the background tint on a {@link View}.
      */
     public static ViewAction setBackgroundTintList(final ColorStateList tint) {
         return new ViewAction() {
@@ -87,6 +89,58 @@
     }
 
     /**
+     * Sets the passed color state list as the image source tint on a {@link View}.
+     */
+    public static ViewAction setImageSourceTintList(final ColorStateList tint) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return anyOf(isAssignableFrom(ImageView.class));
+            }
+
+            @Override
+            public String getDescription() {
+                return "set image source tint list";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+
+                ImageViewCompat.setImageTintList((ImageView) view, tint);
+
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
+
+    /**
+     * Sets the passed mode as the image source tint mode on a <code>View</code>.
+     */
+    public static ViewAction setImageSourceTintMode(final PorterDuff.Mode mode) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return anyOf(isAssignableFrom(ImageView.class));
+            }
+
+            @Override
+            public String getDescription() {
+                return "set image source tint mode";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+
+                ImageViewCompat.setImageTintMode((ImageView) view, mode);
+
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
+
+    /**
      * Sets background drawable on a <code>View</code> that implements the
      * <code>TintableBackgroundView</code> interface.
      */
@@ -140,4 +194,32 @@
         };
     }
 
+    /**
+     * Sets image resource on a <code>View</code> that implements the
+     * <code>TintableBackgroundView</code> interface and also extends the
+     * <code>ImageView</code> base class.
+     */
+    public static ViewAction setImageResource(final @DrawableRes int resId) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return allOf(TestUtilsMatchers.isTintableBackgroundView(),
+                        isAssignableFrom(ImageView.class));
+            }
+
+            @Override
+            public String getDescription() {
+                return "set image resource";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+
+                ((ImageView) view).setImageResource(resId);
+
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
 }
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java
new file mode 100755
index 0000000..c8398ed
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.v7.testutils.TestUtilsActions.setEnabled;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.AppCompatTintableViewActions;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.testutils.TestUtils;
+import android.widget.ImageView;
+
+import org.junit.Test;
+
+/**
+ * In addition to all tinting-related tests done by the base class, this class provides
+ * testing for tinting image resources on appcompat-v7 view classes that extend directly
+ * or indirectly the core {@link ImageView} class.
+ */
+public abstract class AppCompatBaseImageViewTest<T extends ImageView>
+        extends AppCompatBaseViewTest<BaseTestActivity, T> {
+    public AppCompatBaseImageViewTest(Class clazz) {
+        super(clazz);
+    }
+
+    private void verifyImageSourceIsColoredAs(String description, @NonNull ImageView imageView,
+            @ColorInt int color, int allowedComponentVariance) {
+        Drawable imageSource = imageView.getDrawable();
+        TestUtils.assertAllPixelsOfColor(description,
+                imageSource, imageSource.getIntrinsicWidth(), imageSource.getIntrinsicHeight(),
+                true, color, allowedComponentVariance, false);
+    }
+
+    /**
+     * This method tests that image tinting is applied to tintable image view
+     * in enabled and disabled state across a number of <code>ColorStateList</code>s set as
+     * image source tint lists on the same image source.
+     */
+    @Test
+    @SmallTest
+    public void testImageTintingAcrossStateChange() {
+        final @IdRes int viewId = R.id.view_tinted_source;
+        final Resources res = getActivity().getResources();
+        final T view = (T) mContainer.findViewById(viewId);
+
+        @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+        @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+        @ColorInt int sandDefault = ResourcesCompat.getColor(res, R.color.sand_default, null);
+        @ColorInt int sandDisabled = ResourcesCompat.getColor(res, R.color.sand_disabled, null);
+        @ColorInt int oceanDefault = ResourcesCompat.getColor(res, R.color.ocean_default, null);
+        @ColorInt int oceanDisabled = ResourcesCompat.getColor(res, R.color.ocean_disabled, null);
+
+        // Test the default state for tinting set up in the layout XML file.
+        verifyImageSourceIsColoredAs("Default lilac tinting in enabled state", view,
+                lilacDefault, 0);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("Default lilac tinting in disabled state", view,
+                lilacDisabled, 0);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state", view,
+                lilacDefault, 0);
+
+        // Load a new color state list, set it on the view and check that the image has
+        // switched to the matching entry in newly set color state list.
+        final ColorStateList sandColor = ResourcesCompat.getColorStateList(
+                res, R.color.color_state_list_sand, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintList(sandColor));
+        verifyImageSourceIsColoredAs("New sand tinting in enabled state", view,
+                sandDefault, 0);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("New sand tinting in disabled state", view,
+                sandDisabled, 0);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("New sand tinting in re-enabled state", view,
+                sandDefault, 0);
+
+        // Load another color state list, set it on the view and check that the image has
+        // switched to the matching entry in newly set color state list.
+        final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
+                res, R.color.color_state_list_ocean, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintList(oceanColor));
+        verifyImageSourceIsColoredAs("New ocean tinting in enabled state", view,
+                oceanDefault, 0);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("New ocean tinting in disabled state", view,
+                oceanDisabled, 0);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("New ocean tinting in re-enabled state", view,
+                oceanDefault, 0);
+    }
+
+    /**
+     * This method tests that image tinting is applied to tintable image view
+     * in enabled and disabled state across the same image source respects the currently set
+     * image source tinting mode.
+     */
+    @Test
+    @SmallTest
+    public void testImageTintingAcrossModeChange() {
+        final @IdRes int viewId = R.id.view_untinted_source;
+        final Resources res = getActivity().getResources();
+        final T view = (T) mContainer.findViewById(viewId);
+
+        @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+                res, R.color.emerald_translucent_default, null);
+        @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+                res, R.color.emerald_translucent_disabled, null);
+        // This is the fill color of R.drawable.test_drawable_blue set on our view
+        // that we'll be testing in this method
+        @ColorInt int sourceColor = ResourcesCompat.getColor(
+                res, R.color.test_blue, null);
+
+        // Test the default state for tinting set up in the layout XML file.
+        verifyImageSourceIsColoredAs("Default no tinting in enabled state", view,
+                sourceColor, 0);
+
+        // From this point on in this method we're allowing a margin of error in checking the
+        // color of the image source. This is due to both translucent colors being used
+        // in the color state list and off-by-one discrepancies of SRC_OVER when it's compositing
+        // translucent color on top of solid fill color. This is where the allowed variance
+        // value of 2 comes from - one for compositing and one for color translucency.
+        final int allowedComponentVariance = 2;
+
+        // Set src_in tint mode on our view
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_IN));
+
+        // Load a new color state list, set it on the view and check that the image has
+        // switched to the matching entry in newly set color state list.
+        final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
+                res, R.color.color_state_list_emerald_translucent, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintList(emeraldColor));
+        verifyImageSourceIsColoredAs("New emerald tinting in enabled state under src_in", view,
+                emeraldDefault, allowedComponentVariance);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("New emerald tinting in disabled state under src_in", view,
+                emeraldDisabled, allowedComponentVariance);
+
+        // Set src_over tint mode on our view. As the currently set tint list is using
+        // translucent colors, we expect the actual image source of the view to be different under
+        // this new mode (unlike src_in and src_over that behave identically when the destination is
+        // a fully filled rectangle and the source is an opaque color).
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_OVER));
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("New emerald tinting in enabled state under src_over", view,
+                ColorUtils.compositeColors(emeraldDefault, sourceColor),
+                allowedComponentVariance);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the newly set color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("New emerald tinting in disabled state under src_over",
+                view, ColorUtils.compositeColors(emeraldDisabled, sourceColor),
+                allowedComponentVariance);
+    }
+
+    /**
+     * This method tests that opaque tinting applied to tintable image source
+     * is applied correctly after changing the image source itself.
+     */
+    @Test
+    @SmallTest
+    public void testImageOpaqueTintingAcrossImageChange() {
+        final @IdRes int viewId = R.id.view_tinted_no_source;
+        final Resources res = getActivity().getResources();
+        final T view = (T) mContainer.findViewById(viewId);
+
+        @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+        @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+
+        // Set image source on our view
+        onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+                R.drawable.test_drawable_green));
+
+        // Test the default state for tinting set up in the layout XML file.
+        verifyImageSourceIsColoredAs("Default lilac tinting in enabled state on green source",
+                view, lilacDefault, 0);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("Default lilac tinting in disabled state on green source",
+                view, lilacDisabled, 0);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state on green source",
+                view, lilacDefault, 0);
+
+        // Set a different image source on our view based on resource ID
+        onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+                R.drawable.test_drawable_red));
+
+        // Test the default state for tinting set up in the layout XML file.
+        verifyImageSourceIsColoredAs("Default lilac tinting in enabled state on red source",
+                view, lilacDefault, 0);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("Default lilac tinting in disabled state on red source",
+                view, lilacDisabled, 0);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state on red source",
+                view, lilacDefault, 0);
+    }
+
+    /**
+     * This method tests that translucent tinting applied to tintable image source
+     * is applied correctly after changing the image source itself.
+     */
+    @Test
+    @SmallTest
+    public void testImageTranslucentTintingAcrossImageChange() {
+        final @IdRes int viewId = R.id.view_untinted_no_source;
+        final Resources res = getActivity().getResources();
+        final T view = (T) mContainer.findViewById(viewId);
+
+        @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+                res, R.color.emerald_translucent_default, null);
+        @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+                res, R.color.emerald_translucent_disabled, null);
+        // This is the fill color of R.drawable.test_drawable_green that will be set on our view
+        // that we'll be testing in this method
+        @ColorInt int colorGreen = ResourcesCompat.getColor(
+                res, R.color.test_green, null);
+        // This is the fill color of R.drawable.test_drawable_red that will be set on our view
+        // that we'll be testing in this method
+        @ColorInt int colorRed = ResourcesCompat.getColor(
+                res, R.color.test_red, null);
+
+        // Set src_over tint mode on our view. As the currently set tint list is using
+        // translucent colors, we expect the actual image source of the view to be different under
+        // this new mode (unlike src_in and src_over that behave identically when the destination is
+        // a fully filled rectangle and the source is an opaque color).
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_OVER));
+        // Load and set a translucent color state list as the image source tint list
+        final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
+                res, R.color.color_state_list_emerald_translucent, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintList(emeraldColor));
+
+        // Set image source on our view
+        onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+                R.drawable.test_drawable_green));
+
+        // From this point on in this method we're allowing a margin of error in checking the
+        // color of the image source. This is due to both translucent colors being used
+        // in the color state list and off-by-one discrepancies of SRC_OVER when it's compositing
+        // translucent color on top of solid fill color. This is where the allowed variance
+        // value of 2 comes from - one for compositing and one for color translucency.
+        final int allowedComponentVariance = 2;
+
+        // Test the default state for tinting set up with the just loaded tint list.
+        verifyImageSourceIsColoredAs("Emerald tinting in enabled state on green source",
+                view, ColorUtils.compositeColors(emeraldDefault, colorGreen),
+                allowedComponentVariance);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("Emerald tinting in disabled state on green source",
+                view, ColorUtils.compositeColors(emeraldDisabled, colorGreen),
+                allowedComponentVariance);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in the default color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("Emerald tinting in re-enabled state on green source",
+                view, ColorUtils.compositeColors(emeraldDefault, colorGreen),
+                allowedComponentVariance);
+
+        // Set a different image source on our view based on resource ID
+        onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+                R.drawable.test_drawable_red));
+
+        // Test the default state for tinting the new image with the same color state list
+        verifyImageSourceIsColoredAs("Emerald tinting in enabled state on red source",
+                view, ColorUtils.compositeColors(emeraldDefault, colorRed),
+                allowedComponentVariance);
+
+        // Disable the view and check that the image has switched to the matching entry
+        // in our current color state list.
+        onView(withId(viewId)).perform(setEnabled(false));
+        verifyImageSourceIsColoredAs("Emerald tinting in disabled state on red source",
+                view, ColorUtils.compositeColors(emeraldDisabled, colorRed),
+                allowedComponentVariance);
+
+        // Enable the view and check that the image has switched to the matching entry
+        // in our current color state list.
+        onView(withId(viewId)).perform(setEnabled(true));
+        verifyImageSourceIsColoredAs("Emerald tinting in re-enabled state on red source",
+                view, ColorUtils.compositeColors(emeraldDefault, colorRed),
+                allowedComponentVariance);
+    }
+
+    /**
+     * This method tests that background tinting applied on a tintable image view does not
+     * affect the tinting of the image source.
+     */
+    @Test
+    @SmallTest
+    public void testImageTintingAcrossBackgroundTintingChange() {
+        final @IdRes int viewId = R.id.view_untinted_source;
+        final Resources res = getActivity().getResources();
+        final T view = (T) mContainer.findViewById(viewId);
+
+        @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+        @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+        // This is the fill color of R.drawable.test_drawable_blue set on our view
+        // that we'll be testing in this method
+        @ColorInt int sourceColor = ResourcesCompat.getColor(
+                res, R.color.test_blue, null);
+        @ColorInt int newSourceColor = ResourcesCompat.getColor(
+                res, R.color.test_red, null);
+
+        // Test the default state for tinting set up in the layout XML file.
+        verifyImageSourceIsColoredAs("Default no tinting in enabled state", view,
+                sourceColor, 0);
+
+        // Change background tinting of our image
+        final ColorStateList lilacColor = ResourcesCompat.getColorStateList(
+                mResources, R.color.color_state_list_lilac, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setBackgroundResource(
+                        R.drawable.test_background_green));
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setBackgroundTintMode(PorterDuff.Mode.SRC_IN));
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setBackgroundTintList(lilacColor));
+
+        // Verify that the image still has the original color (untinted)
+        verifyImageSourceIsColoredAs("No image tinting after change in background tinting", view,
+                sourceColor, 0);
+
+        // Now set a different image source
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageResource(R.drawable.test_drawable_red));
+        // And verify that the image has the new color (untinted)
+        verifyImageSourceIsColoredAs("No image tinting after change of image source", view,
+                newSourceColor, 0);
+
+        // Change the background tinting again
+        final ColorStateList sandColor = ResourcesCompat.getColorStateList(
+                mResources, R.color.color_state_list_sand, null);
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setBackgroundTintList(sandColor));
+        // And verify that the image still has the same new color (untinted)
+        verifyImageSourceIsColoredAs("No image tinting after change in background tinting", view,
+                newSourceColor, 0);
+
+        // Now set up image tinting on our view. We're using a color state list with fully
+        // opaque colors, and we expect the matching entry in that list to be applied on the
+        // image source (ignoring the background tinting)
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_IN));
+        onView(withId(viewId)).perform(
+                AppCompatTintableViewActions.setImageSourceTintList(lilacColor));
+        verifyImageSourceIsColoredAs("New lilac image tinting", view,
+                lilacDefault, 0);
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
index bacb0e3..6e4e6c0 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
@@ -35,6 +35,7 @@
 import android.support.annotation.ColorInt;
 import android.support.annotation.IdRes;
 import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
 import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.graphics.ColorUtils;
 import android.support.v7.app.BaseInstrumentationTestCase;
@@ -42,7 +43,6 @@
 import android.support.v7.testutils.AppCompatTintableViewActions;
 import android.support.v7.testutils.BaseTestActivity;
 import android.support.v7.testutils.TestUtils;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.view.View;
 import android.view.ViewGroup;
 
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java
new file mode 100644
index 0000000..1a64047
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatImageButtonActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.appcompat_imagebutton_activity;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java
new file mode 100644
index 0000000..67fca4a
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+/**
+ * In addition to all tinting-related tests done by the base class, this class provides
+ * tests specific to {@link AppCompatImageButton} class.
+ */
+public class AppCompatImageButtonTest extends AppCompatBaseImageViewTest<AppCompatImageButton> {
+    public AppCompatImageButtonTest() {
+        super(AppCompatImageButtonActivity.class);
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
index d6801da..88d105c 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
@@ -31,8 +31,7 @@
  * tests specific to {@link AppCompatImageView} class.
  */
 @SmallTest
-public class AppCompatImageViewTest
-        extends AppCompatBaseViewTest<AppCompatImageViewActivity, AppCompatImageView> {
+public class AppCompatImageViewTest extends AppCompatBaseImageViewTest<AppCompatImageView> {
     public AppCompatImageViewTest() {
         super(AppCompatImageViewActivity.class);
     }
@@ -40,7 +39,7 @@
     @Test
     public void testImageViewBothSrcCompatAndroidSrcSet() {
         final int expectedColor = mContainer.getResources().getColor(R.color.test_blue);
-        ViewInteraction imageViewInteration = onView(withId(R.id.view_android_src_srccompat));
-        imageViewInteration.check(matches(TestUtilsMatchers.drawable(expectedColor)));
+        ViewInteraction imageViewInteraction = onView(withId(R.id.view_android_src_srccompat));
+        imageViewInteraction.check(matches(TestUtilsMatchers.drawable(expectedColor)));
     }
 }
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index fe16de8..0d85b3b 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -2708,7 +2708,8 @@
                     record.updatePlaybackInfo();
                 }
                 if (mMediaSession != null) {
-                    if (mSelectedRoute == getDefaultRoute()) {
+                    if (mSelectedRoute == getDefaultRoute()
+                            || mSelectedRoute == getBluetoothRoute()) {
                         // Local route
                         mMediaSession.clearVolumeHandling();
                     } else {
diff --git a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
index 9392ee2..e72d48d 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
@@ -283,7 +283,7 @@
                 position, false, deadlineNs);
 
         if (holder != null) {
-            if (holder.isBound()) {
+            if (holder.isBound() && !holder.isInvalid()) {
                 // Only give the view a chance to go into the cache if binding succeeded
                 // Note that we must use public method, since item may need cleanup
                 recycler.recycleView(holder.itemView);
@@ -335,7 +335,10 @@
         long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
         RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
                 task.position, taskDeadlineNs);
-        if (holder != null && holder.mNestedRecyclerView != null) {
+        if (holder != null
+                && holder.mNestedRecyclerView != null
+                && holder.isBound()
+                && !holder.isInvalid()) {
             prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
         }
     }
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
index 9e262db..c74e20d 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
@@ -170,11 +170,7 @@
         }
         int distance =
                 Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
-        if (distance > 0) {
-            return (int) Math.floor(distance / distancePerChild);
-        } else {
-            return (int) Math.ceil(distance / distancePerChild);
-        }
+        return (int) Math.round(distance / distancePerChild);
     }
 
     /**
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 86a4127..5e0bc9e 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -4119,31 +4119,16 @@
     /**
      * Call this method to signal that *all* adapter content has changed (generally, because of
      * swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should
-     * be discarded or animated. Note that this work is deferred because RecyclerView requires a
-     * layout to resolve non-incremental changes to the data set.
+     * be discarded or animated.
      *
-     * Attached items are labeled as position unknown, and may no longer be cached.
+     * Attached items are labeled as invalid, and all cached items are discarded.
      *
      * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
-     * so calling this method *must* be associated with marking the cache invalid, so that the
-     * only valid items that remain in the cache, once layout occurs, are prefetched items.
+     * so this method must always discard all cached views so that the only valid items that remain
+     * in the cache, once layout occurs, are valid prefetched items.
      */
     void setDataSetChangedAfterLayout() {
-        if (mDataSetHasChangedAfterLayout) {
-            return;
-        }
         mDataSetHasChangedAfterLayout = true;
-        final int childCount = mChildHelper.getUnfilteredChildCount();
-        for (int i = 0; i < childCount; i++) {
-            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
-            if (holder != null && !holder.shouldIgnore()) {
-                holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
-            }
-        }
-        mRecycler.setAdapterPositionsAsUnknown();
-
-        // immediately mark all views as invalid, so prefetched views can be
-        // differentiated from views bound to previous data set - both in children, and cache
         markKnownViewsInvalid();
     }
 
@@ -6178,16 +6163,6 @@
             }
         }
 
-        void setAdapterPositionsAsUnknown() {
-            final int cachedCount = mCachedViews.size();
-            for (int i = 0; i < cachedCount; i++) {
-                final ViewHolder holder = mCachedViews.get(i);
-                if (holder != null) {
-                    holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
-                }
-            }
-        }
-
         void markKnownViewsInvalid() {
             if (mAdapter != null && mAdapter.hasStableIds()) {
                 final int cachedCount = mCachedViews.size();
@@ -11561,7 +11536,6 @@
         void prepareForNestedPrefetch(Adapter adapter) {
             mLayoutStep = STEP_START;
             mItemCount = adapter.getItemCount();
-            mStructureChanged = false;
             mInPreLayout = false;
             mTrackOldChangeHolders = false;
             mIsMeasuring = false;
@@ -11749,7 +11723,7 @@
          * @param velocityX the fling velocity on the X axis
          * @param velocityY the fling velocity on the Y axis
          *
-         * @return true if the fling washandled, false otherwise.
+         * @return true if the fling was handled, false otherwise.
          */
         public abstract boolean onFling(int velocityX, int velocityY);
     }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 2da67af..ae70900 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -31,7 +31,9 @@
 import android.graphics.Color;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
 import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.v4.view.AccessibilityDelegateCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
@@ -402,6 +404,12 @@
         }
     }
 
+    // Run this test on Jelly Bean and newer because clearFocus on API 15 will call
+    // requestFocus in ViewRootImpl when clearChildFocus is called. Whereas, in API 16 and above,
+    // this call is delayed until after onFocusChange callback is called. Thus on API 16+, there's a
+    // transient state of no child having focus during which onFocusChange is executed. This
+    // transient state does not exist on API 15-.
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
     @Test
     public void unfocusableScrollingWhenFocusCleared() throws Throwable {
         // The maximum number of child views that can be visible at any time.
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
index 1b85884..f682593 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -811,6 +812,58 @@
     }
 
     @Test
+    public void nestedPrefetchNotClearInnerStructureChangeFlag() {
+        LinearLayoutManager llm = new LinearLayoutManager(getContext());
+        assertEquals(2, llm.getInitialPrefetchItemCount());
+
+        mRecyclerView.setLayoutManager(llm);
+        mRecyclerView.setAdapter(new OuterAdapter());
+
+        layout(200, 200);
+        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+
+        // prefetch 2 (default)
+        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
+        assertNotNull(holder);
+        assertNotNull(holder.mNestedRecyclerView);
+        RecyclerView innerView = holder.mNestedRecyclerView.get();
+        RecyclerView.Adapter innerAdapter = innerView.getAdapter();
+        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
+        // mStructureChanged is initially true before first layout pass.
+        assertTrue(innerView.mState.mStructureChanged);
+        assertTrue(innerView.hasPendingAdapterUpdates());
+
+        // layout position 2 and clear mStructureChanged
+        mRecyclerView.scrollToPosition(2);
+        layout(200, 200);
+        mRecyclerView.scrollToPosition(0);
+        layout(200, 200);
+        assertFalse(innerView.mState.mStructureChanged);
+        assertFalse(innerView.hasPendingAdapterUpdates());
+
+        // notify change on the cached innerView.
+        innerAdapter.notifyDataSetChanged();
+        assertTrue(innerView.mState.mStructureChanged);
+        assertTrue(innerView.hasPendingAdapterUpdates());
+
+        // prefetch again
+        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+        ((LinearLayoutManager) innerView.getLayoutManager())
+                .setInitialPrefetchItemCount(2);
+        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
+
+        // The re-prefetch is not necessary get the same inner view but we will get same Adapter
+        holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
+        innerView = holder.mNestedRecyclerView.get();
+        assertSame(innerAdapter, innerView.getAdapter());
+        // prefetch shouldn't clear the mStructureChanged flag
+        assertTrue(innerView.mState.mStructureChanged);
+        assertTrue(innerView.hasPendingAdapterUpdates());
+    }
+
+    @Test
     public void nestedPrefetchReverseInner() {
         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
         mRecyclerView.setAdapter(new OuterAdapter(/* reverseInner = */ true));
@@ -1066,8 +1119,10 @@
 
         @Override
         public void onViewRecycled(ViewHolder holder) {
-            mSavedStates.set(holder.getAdapterPosition(),
-                    holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
+            if (holder.getAdapterPosition() >= 0) {
+                mSavedStates.set(holder.getAdapterPosition(),
+                        holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
+            }
         }
 
         @Override
@@ -1122,4 +1177,55 @@
         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
         CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 0, 1);
     }
+
+
+    @Test
+    public void nestedPrefetchDiscardStalePrefetch() {
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
+        mRecyclerView.setAdapter(outerAdapter);
+
+        // zero cache, so item we prefetch can't already be ready
+        mRecyclerView.setItemViewCacheSize(0);
+
+        // layout as 2x2, starting on row index 2, with empty cache
+        layout(200, 200);
+        mRecyclerView.scrollBy(0, 200);
+
+        // no views cached, or previously used (so we can trust number in mItemsBound)
+        mRecycler.mRecyclerPool.clear();
+        assertEquals(0, mRecycler.mRecyclerPool.getRecycledViewCount(0));
+        assertEquals(0, mRecycler.mCachedViews.size());
+
+        // prefetch the outer item and its inner children
+        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+
+        // 4 is prefetched with 2 inner children, first two binds
+        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
+        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4);
+        assertNotNull(holder);
+        assertNotNull(holder.mNestedRecyclerView);
+        RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
+        assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
+        assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
+        assertEquals(2, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
+
+        // notify data set changed, so any previously prefetched items invalid, and re-prefetch
+        innerRecyclerView.getAdapter().notifyDataSetChanged();
+        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+
+        // 4 is prefetched again...
+        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
+
+        // reusing the same instance with 2 inner children...
+        assertSame(holder, CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4));
+        assertSame(innerRecyclerView, holder.mNestedRecyclerView.get());
+        assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
+        assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
+
+        // ... but there should be two new binds
+        assertEquals(4, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
+    }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 75d59c8..6fd9a86 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -345,7 +345,9 @@
             public View onFocusSearchFailed(View focused, int direction,
                     RecyclerView.Recycler recycler,
                     RecyclerView.State state) {
-                assertEquals(View.FOCUS_FORWARD, direction);
+                int expectedDir = Build.VERSION.SDK_INT <= 15 ? View.FOCUS_DOWN :
+                        View.FOCUS_FORWARD;
+                assertEquals(expectedDir, direction);
                 assertEquals(1, getChildCount());
                 View child0 = getChildAt(0);
                 View view = recycler.getViewForPosition(1);