Merge commit 'c8bb87bf4df1a1cb63a1570c74e6db8c5ec8d524' from
nyc-support-25.3-dev to mirror-aosp-master.

Bug: 36221697
Test: build
Change-Id: I08f556bd2d207a028ed3ae7bd1f726e46843520c
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index b24c09a..3e9e37b 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -5,4 +5,4 @@
     <mapping directory="$PROJECT_DIR$/../../external/doclava" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/../../external/jdiff" vcs="Git" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/Android.mk b/Android.mk
index 466559b..fd17ee6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -57,33 +57,12 @@
 $(call all-named-files-under-exclude,$(1),$(2),.)
 endef
 
-# Proxy to gradle task for updating API
-.PHONY: update-support-api
-update-support-api: PRIVATE_LOCAL_PATH := $(LOCAL_PATH)
-update-support-api:
-	$(PRIVATE_LOCAL_PATH)/gradlew -p $(PRIVATE_LOCAL_PATH) updateApi
-
-# Proxy to gradle task for checking API
-.PHONY: check-support-api
-check-support-api: PRIVATE_LOCAL_PATH := $(LOCAL_PATH)
-check-support-api:
-	$(PRIVATE_LOCAL_PATH)/gradlew -p $(PRIVATE_LOCAL_PATH) checkApi
-
-# Proxy to gradle task for generating docs
-.PHONY: support-docs
-support-docs: PRIVATE_LOCAL_PATH := $(LOCAL_PATH)
-support-docs:
-	$(PRIVATE_LOCAL_PATH)/gradlew -p $(PRIVATE_LOCAL_PATH) generateDocs
-
 # Pre-process support library AIDLs
 aidl_files := $(addprefix $(LOCAL_PATH)/, $(call all-subdir-named-files-exclude,*.aidl,I*.aidl))
 support-aidl := $(TARGET_OUT_COMMON_INTERMEDIATES)/support.aidl
 $(support-aidl): $(aidl_files) | $(AIDL)
 	$(AIDL) --preprocess $@ $(aidl_files)
 
-# Check APIs and generate support AIDL file for SDK build
-sdk: check-support-api $(support-aidl)
-
 # Build all support libraries
 include $(call all-makefiles-under,$(LOCAL_PATH))
 
diff --git a/api/25.2.0.txt b/api/25.3.0.txt
similarity index 97%
rename from api/25.2.0.txt
rename to api/25.3.0.txt
index 6709b97..917fd00 100644
--- a/api/25.2.0.txt
+++ b/api/25.3.0.txt
@@ -354,11 +354,18 @@
     method public android.content.res.ColorStateList getItemTextColor();
     method public int getMaxItemCount();
     method public android.view.Menu getMenu();
+    method public int getSelectedItemId();
     method public void inflateMenu(int);
     method public void setItemBackgroundResource(int);
     method public void setItemIconTintList(android.content.res.ColorStateList);
     method public void setItemTextColor(android.content.res.ColorStateList);
+    method public void setOnNavigationItemReselectedListener(android.support.design.widget.BottomNavigationView.OnNavigationItemReselectedListener);
     method public void setOnNavigationItemSelectedListener(android.support.design.widget.BottomNavigationView.OnNavigationItemSelectedListener);
+    method public void setSelectedItemId(int);
+  }
+
+  public static abstract interface BottomNavigationView.OnNavigationItemReselectedListener {
+    method public abstract void onNavigationItemReselected(android.view.MenuItem);
   }
 
   public static abstract interface BottomNavigationView.OnNavigationItemSelectedListener {
@@ -1707,6 +1714,7 @@
     method protected java.lang.Object createEntranceTransition();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+    method public android.support.v17.leanback.widget.DetailsParallax getParallax();
     method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
     method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
     method protected void onEntranceTransitionEnd();
@@ -1724,11 +1732,30 @@
     method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
   }
 
+  public class DetailsFragmentBackgroundController {
+    ctor public DetailsFragmentBackgroundController(android.support.v17.leanback.app.DetailsFragment);
+    method public void enableParallax();
+    method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+    method public final android.app.Fragment findOrCreateVideoFragment();
+    method public final android.graphics.drawable.Drawable getBottomDrawable();
+    method public final android.graphics.Bitmap getCoverBitmap();
+    method public final android.graphics.drawable.Drawable getCoverDrawable();
+    method public final int getParallaxDrawableMaxOffset();
+    method public final int getSolidColor();
+    method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+    method public android.app.Fragment onCreateVideoFragment();
+    method public final void setCoverBitmap(android.graphics.Bitmap);
+    method public final void setParallaxDrawableMaxOffset(int);
+    method public final void setSolidColor(int);
+    method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+  }
+
   public class DetailsSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
     ctor public DetailsSupportFragment();
     method protected java.lang.Object createEntranceTransition();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+    method public android.support.v17.leanback.widget.DetailsParallax getParallax();
     method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
     method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
     method protected void onEntranceTransitionEnd();
@@ -1746,6 +1773,24 @@
     method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
   }
 
+  public class DetailsSupportFragmentBackgroundController {
+    ctor public DetailsSupportFragmentBackgroundController(android.support.v17.leanback.app.DetailsSupportFragment);
+    method public void enableParallax();
+    method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+    method public final android.support.v4.app.Fragment findOrCreateVideoSupportFragment();
+    method public final android.graphics.drawable.Drawable getBottomDrawable();
+    method public final android.graphics.Bitmap getCoverBitmap();
+    method public final android.graphics.drawable.Drawable getCoverDrawable();
+    method public final int getParallaxDrawableMaxOffset();
+    method public final int getSolidColor();
+    method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+    method public android.support.v4.app.Fragment onCreateVideoSupportFragment();
+    method public final void setCoverBitmap(android.graphics.Bitmap);
+    method public final void setParallaxDrawableMaxOffset(int);
+    method public final void setSolidColor(int);
+    method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+  }
+
   public class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
     ctor public ErrorFragment();
     method public android.graphics.drawable.Drawable getBackgroundDrawable();
@@ -2040,6 +2085,7 @@
     method public void setFadingEnabled(boolean);
     method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
     method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+    method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
     method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
     method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
     method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
@@ -2124,6 +2170,7 @@
     method public void setFadingEnabled(boolean);
     method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
     method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+    method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
     method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
     method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
     method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
@@ -2290,6 +2337,28 @@
     method public void setSelectedPosition(int);
   }
 
+  public class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
+    ctor public VideoFragment();
+    method public android.view.SurfaceView getSurfaceView();
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+    ctor public VideoFragmentGlueHost(android.support.v17.leanback.app.VideoFragment);
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoSupportFragment extends android.support.v17.leanback.app.PlaybackSupportFragment {
+    ctor public VideoSupportFragment();
+    method public android.view.SurfaceView getSurfaceView();
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoSupportFragmentGlueHost extends android.support.v17.leanback.app.PlaybackSupportFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+    ctor public VideoSupportFragmentGlueHost(android.support.v17.leanback.app.VideoSupportFragment);
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
 }
 
 package android.support.v17.leanback.database {
@@ -2305,6 +2374,26 @@
 
 package android.support.v17.leanback.graphics {
 
+  public class BoundsRule {
+    ctor public BoundsRule();
+    ctor public BoundsRule(android.support.v17.leanback.graphics.BoundsRule);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule absoluteValue(int);
+    method public void calculateBounds(android.graphics.Rect, android.graphics.Rect);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParent(float);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParentWithOffset(float, int);
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule bottom;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule left;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule right;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule top;
+  }
+
+  public static final class BoundsRule.ValueRule {
+    method public int getAbsoluteValue();
+    method public float getFraction();
+    method public void setAbsoluteValue(int);
+    method public void setFraction(float);
+  }
+
   public final class ColorFilterCache {
     method public static android.support.v17.leanback.graphics.ColorFilterCache getColorFilterCache(int);
     method public android.graphics.ColorFilter getFilterForLevel(float);
@@ -2331,6 +2420,54 @@
     method public void setActiveLevel(float);
   }
 
+  public class CompositeDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
+    ctor public CompositeDrawable();
+    method public void addChildDrawable(android.graphics.drawable.Drawable);
+    method public void draw(android.graphics.Canvas);
+    method public android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable getChildAt(int);
+    method public int getChildCount();
+    method public android.graphics.drawable.Drawable getDrawable(int);
+    method public int getOpacity();
+    method public void invalidateDrawable(android.graphics.drawable.Drawable);
+    method public void removeChild(int);
+    method public void removeDrawable(android.graphics.drawable.Drawable);
+    method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long);
+    method public void setAlpha(int);
+    method public void setChildDrawableAt(int, android.graphics.drawable.Drawable);
+    method public void setColorFilter(android.graphics.ColorFilter);
+    method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
+  }
+
+  public static final class CompositeDrawable.ChildDrawable {
+    ctor public CompositeDrawable.ChildDrawable(android.graphics.drawable.Drawable, android.support.v17.leanback.graphics.CompositeDrawable);
+    method public android.support.v17.leanback.graphics.BoundsRule getBoundsRule();
+    method public android.graphics.drawable.Drawable getDrawable();
+    method public void recomputeBounds();
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> BOTTOM_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> BOTTOM_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> LEFT_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> LEFT_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> RIGHT_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> RIGHT_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> TOP_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> TOP_FRACTION;
+  }
+
+  public class FitWidthBitmapDrawable extends android.graphics.drawable.Drawable {
+    ctor public FitWidthBitmapDrawable();
+    method public void draw(android.graphics.Canvas);
+    method public android.graphics.Bitmap getBitmap();
+    method public int getOpacity();
+    method public android.graphics.Rect getSource();
+    method public int getVerticalOffset();
+    method public void setAlpha(int);
+    method public void setBitmap(android.graphics.Bitmap);
+    method public void setColorFilter(android.graphics.ColorFilter);
+    method public void setSource(android.graphics.Rect);
+    method public void setVerticalOffset(int);
+    field public static final android.util.Property<android.support.v17.leanback.graphics.FitWidthBitmapDrawable, java.lang.Integer> PROPERTY_VERTICAL_OFFSET;
+  }
+
 }
 
 package android.support.v17.leanback.media {
@@ -2727,6 +2864,12 @@
     field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDetailsDescriptionViewHolder;
   }
 
+  public class DetailsParallax extends android.support.v17.leanback.widget.RecyclerViewParallax {
+    ctor public DetailsParallax();
+    method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowBottom();
+    method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowTop();
+  }
+
   public class DividerPresenter extends android.support.v17.leanback.widget.Presenter {
     ctor public DividerPresenter();
     method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
@@ -3347,6 +3490,115 @@
     method public final boolean isRenderedAsRowView();
   }
 
+  public abstract class Parallax<PropertyT extends android.util.Property> {
+    ctor public Parallax();
+    method public void addEffect(android.support.v17.leanback.widget.ParallaxEffect);
+    method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue...);
+    method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue...);
+    method public abstract PropertyT addProperty(java.lang.String);
+    method public abstract PropertyT createProperty(java.lang.String, int);
+    method public java.util.List<android.support.v17.leanback.widget.ParallaxEffect> getEffects();
+    method public final java.util.List<PropertyT> getProperties();
+    method public void removeAllEffects();
+    method public void removeEffect(android.support.v17.leanback.widget.ParallaxEffect);
+    method public void updateValues();
+    method public abstract void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static abstract class Parallax.FloatParallax<FloatPropertyT extends android.support.v17.leanback.widget.Parallax.FloatProperty> extends android.support.v17.leanback.widget.Parallax {
+    ctor public Parallax.FloatParallax();
+    method public final FloatPropertyT addProperty(java.lang.String);
+    method public abstract float getMaxValue();
+    method public final float getPropertyValue(int);
+    method public final void setPropertyValue(int, float);
+    method public final void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static class Parallax.FloatProperty extends android.util.Property {
+    ctor public Parallax.FloatProperty(java.lang.String, int);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue at(float, float);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue atAbsolute(float);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue atFraction(float);
+    method public final java.lang.Float get(android.support.v17.leanback.widget.Parallax.FloatParallax);
+    method public final int getIndex();
+    method public final void set(android.support.v17.leanback.widget.Parallax.FloatParallax, java.lang.Float);
+    field public static final float UNKNOWN_AFTER = 3.4028235E38f;
+    field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
+  }
+
+  public static class Parallax.FloatPropertyMarkerValue extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue {
+    ctor public Parallax.FloatPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.FloatProperty, float);
+    ctor public Parallax.FloatPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.FloatProperty, float, float);
+    method public final float getMarkerValue(android.support.v17.leanback.widget.Parallax.FloatParallax);
+  }
+
+  public static abstract class Parallax.IntParallax<IntPropertyT extends android.support.v17.leanback.widget.Parallax.IntProperty> extends android.support.v17.leanback.widget.Parallax {
+    ctor public Parallax.IntParallax();
+    method public final IntPropertyT addProperty(java.lang.String);
+    method public abstract int getMaxValue();
+    method public final int getPropertyValue(int);
+    method public final void setPropertyValue(int, int);
+    method public final void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static class Parallax.IntProperty extends android.util.Property {
+    ctor public Parallax.IntProperty(java.lang.String, int);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue at(int, float);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue atAbsolute(int);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue atFraction(float);
+    method public final java.lang.Integer get(android.support.v17.leanback.widget.Parallax.IntParallax);
+    method public final int getIndex();
+    method public final void set(android.support.v17.leanback.widget.Parallax.IntParallax, java.lang.Integer);
+    field public static final int UNKNOWN_AFTER = 2147483647; // 0x7fffffff
+    field public static final int UNKNOWN_BEFORE = -2147483648; // 0x80000000
+  }
+
+  public static class Parallax.IntPropertyMarkerValue extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue {
+    ctor public Parallax.IntPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.IntProperty, int);
+    ctor public Parallax.IntPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.IntProperty, int, float);
+    method public final int getMarkerValue(android.support.v17.leanback.widget.Parallax.IntParallax);
+  }
+
+  public static class Parallax.PropertyMarkerValue<PropertyT> {
+    ctor public Parallax.PropertyMarkerValue(PropertyT);
+    method public PropertyT getProperty();
+  }
+
+  public abstract class ParallaxEffect<ParallaxEffectT extends android.support.v17.leanback.widget.ParallaxEffect, PropertyMarkerValueT extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue> {
+    ctor public ParallaxEffect();
+    method public final void addTarget(android.support.v17.leanback.widget.ParallaxTarget);
+    method protected abstract float calculateFraction(android.support.v17.leanback.widget.Parallax);
+    method public final java.util.List<PropertyMarkerValueT> getPropertyRanges();
+    method public final java.util.List<android.support.v17.leanback.widget.ParallaxTarget> getTargets();
+    method public final void performMapping(android.support.v17.leanback.widget.Parallax);
+    method public final void removeTarget(android.support.v17.leanback.widget.ParallaxTarget);
+    method public final void setPropertyRanges(PropertyMarkerValueT...);
+    method public final android.support.v17.leanback.widget.ParallaxEffect target(android.support.v17.leanback.widget.ParallaxTarget);
+    method public final android.support.v17.leanback.widget.ParallaxEffect target(java.lang.Object, android.animation.PropertyValuesHolder);
+  }
+
+  public static final class ParallaxEffect.FloatEffect extends android.support.v17.leanback.widget.ParallaxEffect {
+    ctor public ParallaxEffect.FloatEffect();
+    method protected float calculateFraction(android.support.v17.leanback.widget.Parallax);
+  }
+
+  public static final class ParallaxEffect.IntEffect extends android.support.v17.leanback.widget.ParallaxEffect {
+    ctor public ParallaxEffect.IntEffect();
+    method protected float calculateFraction(android.support.v17.leanback.widget.Parallax);
+  }
+
+  public abstract class ParallaxTarget {
+    ctor public ParallaxTarget();
+    method public abstract float getFraction();
+    method public abstract void update(float);
+  }
+
+  public static final class ParallaxTarget.PropertyValuesHolderTarget extends android.support.v17.leanback.widget.ParallaxTarget {
+    ctor public ParallaxTarget.PropertyValuesHolderTarget(java.lang.Object, android.animation.PropertyValuesHolder);
+    method public float getFraction();
+    method public void update(float);
+  }
+
   public class PlaybackControlsRow extends android.support.v17.leanback.widget.Row {
     ctor public PlaybackControlsRow(java.lang.Object);
     ctor public PlaybackControlsRow();
@@ -3536,6 +3788,25 @@
     method public void unselect();
   }
 
+  public class RecyclerViewParallax extends android.support.v17.leanback.widget.Parallax.IntParallax {
+    ctor public RecyclerViewParallax();
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty createProperty(java.lang.String, int);
+    method public int getMaxValue();
+    method public android.support.v7.widget.RecyclerView getRecyclerView();
+    method public void setRecyclerView(android.support.v7.widget.RecyclerView);
+  }
+
+  public static final class RecyclerViewParallax.ChildPositionProperty extends android.support.v17.leanback.widget.Parallax.IntProperty {
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty adapterPosition(int);
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty fraction(float);
+    method public int getAdapterPosition();
+    method public float getFraction();
+    method public int getOffset();
+    method public int getViewId();
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty offset(int);
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty viewId(int);
+  }
+
   public class Row {
     ctor public Row(long, android.support.v17.leanback.widget.HeaderItem);
     ctor public Row(android.support.v17.leanback.widget.HeaderItem);
@@ -5264,6 +5535,10 @@
     method public static int setAlphaComponent(int, int);
   }
 
+  public final class PaintCompat {
+    method public static boolean hasGlyph(android.graphics.Paint, java.lang.String);
+  }
+
 }
 
 package android.support.v4.graphics.drawable {
diff --git a/api/current.txt b/api/current.txt
index 6709b97..a6cde7b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -135,7 +135,9 @@
   public class CustomTabsCallback {
     ctor public CustomTabsCallback();
     method public void extraCallback(java.lang.String, android.os.Bundle);
+    method public void onMessageChannelReady(android.os.Bundle);
     method public void onNavigationEvent(int, android.os.Bundle);
+    method public void onPostMessage(java.lang.String, android.os.Bundle);
     field public static final int NAVIGATION_ABORTED = 4; // 0x4
     field public static final int NAVIGATION_FAILED = 3; // 0x3
     field public static final int NAVIGATION_FINISHED = 2; // 0x2
@@ -215,10 +217,19 @@
     method protected abstract boolean mayLaunchUrl(android.support.customtabs.CustomTabsSessionToken, android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
     method protected abstract boolean newSession(android.support.customtabs.CustomTabsSessionToken);
     method public android.os.IBinder onBind(android.content.Intent);
+    method protected abstract int postMessage(android.support.customtabs.CustomTabsSessionToken, java.lang.String, android.os.Bundle);
+    method protected abstract boolean requestPostMessageChannel(android.support.customtabs.CustomTabsSessionToken, android.net.Uri);
     method protected abstract boolean updateVisuals(android.support.customtabs.CustomTabsSessionToken, android.os.Bundle);
     method protected abstract boolean warmup(long);
     field public static final java.lang.String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
     field public static final java.lang.String KEY_URL = "android.support.customtabs.otherurls.URL";
+    field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+    field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+    field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+  }
+
+  public static abstract class CustomTabsService.Result implements java.lang.annotation.Annotation {
   }
 
   public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
@@ -229,6 +240,8 @@
 
   public final class CustomTabsSession {
     method public boolean mayLaunchUrl(android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
+    method public int postMessage(java.lang.String, android.os.Bundle);
+    method public boolean requestPostMessageChannel(android.net.Uri);
     method public boolean setActionButton(android.graphics.Bitmap, java.lang.String);
     method public boolean setSecondaryToolbarViews(android.widget.RemoteViews, int[], android.app.PendingIntent);
     method public deprecated boolean setToolbarItem(int, android.graphics.Bitmap, java.lang.String);
@@ -237,6 +250,24 @@
   public class CustomTabsSessionToken {
     method public android.support.customtabs.CustomTabsCallback getCallback();
     method public static android.support.customtabs.CustomTabsSessionToken getSessionTokenFromIntent(android.content.Intent);
+    method public boolean isAssociatedWith(android.support.customtabs.CustomTabsSession);
+  }
+
+  public class PostMessageService extends android.app.Service {
+    ctor public PostMessageService();
+    method public android.os.IBinder onBind(android.content.Intent);
+  }
+
+  public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+    ctor public PostMessageServiceConnection(android.support.customtabs.CustomTabsSessionToken);
+    method public boolean bindSessionToPostMessageService(android.content.Context, java.lang.String);
+    method public final boolean notifyMessageChannelReady(android.os.Bundle);
+    method public void onPostMessageServiceConnected();
+    method public void onPostMessageServiceDisconnected();
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+    method public final void onServiceDisconnected(android.content.ComponentName);
+    method public final boolean postMessage(java.lang.String, android.os.Bundle);
+    method public void unbindFromContext(android.content.Context);
   }
 
 }
@@ -354,11 +385,18 @@
     method public android.content.res.ColorStateList getItemTextColor();
     method public int getMaxItemCount();
     method public android.view.Menu getMenu();
+    method public int getSelectedItemId();
     method public void inflateMenu(int);
     method public void setItemBackgroundResource(int);
     method public void setItemIconTintList(android.content.res.ColorStateList);
     method public void setItemTextColor(android.content.res.ColorStateList);
+    method public void setOnNavigationItemReselectedListener(android.support.design.widget.BottomNavigationView.OnNavigationItemReselectedListener);
     method public void setOnNavigationItemSelectedListener(android.support.design.widget.BottomNavigationView.OnNavigationItemSelectedListener);
+    method public void setSelectedItemId(int);
+  }
+
+  public static abstract interface BottomNavigationView.OnNavigationItemReselectedListener {
+    method public abstract void onNavigationItemReselected(android.view.MenuItem);
   }
 
   public static abstract interface BottomNavigationView.OnNavigationItemSelectedListener {
@@ -867,7 +905,8 @@
     method public java.lang.String getAttribute(java.lang.String);
     method public double getAttributeDouble(java.lang.String, double);
     method public int getAttributeInt(java.lang.String, int);
-    method public boolean getLatLong(float[]);
+    method public deprecated boolean getLatLong(float[]);
+    method public double[] getLatLong();
     method public byte[] getThumbnail();
     method public android.graphics.Bitmap getThumbnailBitmap();
     method public byte[] getThumbnailBytes();
@@ -876,6 +915,7 @@
     method public boolean isThumbnailCompressed();
     method public void saveAttributes() throws java.io.IOException;
     method public void setAttribute(java.lang.String, java.lang.String);
+    method public void setLatLong(double, double);
     field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
     field public static final int ORIENTATION_FLIP_VERTICAL = 4; // 0x4
     field public static final int ORIENTATION_NORMAL = 1; // 0x1
@@ -1514,6 +1554,7 @@
     method public final boolean isHeadersTransitionOnBackEnabled();
     method public boolean isInHeadersTransition();
     method public boolean isShowingHeaders();
+    method public android.support.v17.leanback.app.HeadersFragment onCreateHeadersFragment();
     method protected void onEntranceTransitionEnd();
     method protected void onEntranceTransitionPrepare();
     method protected void onEntranceTransitionStart();
@@ -1618,6 +1659,7 @@
     method public final boolean isHeadersTransitionOnBackEnabled();
     method public boolean isInHeadersTransition();
     method public boolean isShowingHeaders();
+    method public android.support.v17.leanback.app.HeadersSupportFragment onCreateHeadersSupportFragment();
     method protected void onEntranceTransitionEnd();
     method protected void onEntranceTransitionPrepare();
     method protected void onEntranceTransitionStart();
@@ -1707,6 +1749,7 @@
     method protected java.lang.Object createEntranceTransition();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+    method public android.support.v17.leanback.widget.DetailsParallax getParallax();
     method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
     method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
     method protected void onEntranceTransitionEnd();
@@ -1724,11 +1767,30 @@
     method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
   }
 
+  public class DetailsFragmentBackgroundController {
+    ctor public DetailsFragmentBackgroundController(android.support.v17.leanback.app.DetailsFragment);
+    method public void enableParallax();
+    method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+    method public final android.app.Fragment findOrCreateVideoFragment();
+    method public final android.graphics.drawable.Drawable getBottomDrawable();
+    method public final android.graphics.Bitmap getCoverBitmap();
+    method public final android.graphics.drawable.Drawable getCoverDrawable();
+    method public final int getParallaxDrawableMaxOffset();
+    method public final int getSolidColor();
+    method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+    method public android.app.Fragment onCreateVideoFragment();
+    method public final void setCoverBitmap(android.graphics.Bitmap);
+    method public final void setParallaxDrawableMaxOffset(int);
+    method public final void setSolidColor(int);
+    method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+  }
+
   public class DetailsSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
     ctor public DetailsSupportFragment();
     method protected java.lang.Object createEntranceTransition();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+    method public android.support.v17.leanback.widget.DetailsParallax getParallax();
     method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
     method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
     method protected void onEntranceTransitionEnd();
@@ -1746,6 +1808,24 @@
     method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
   }
 
+  public class DetailsSupportFragmentBackgroundController {
+    ctor public DetailsSupportFragmentBackgroundController(android.support.v17.leanback.app.DetailsSupportFragment);
+    method public void enableParallax();
+    method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+    method public final android.support.v4.app.Fragment findOrCreateVideoSupportFragment();
+    method public final android.graphics.drawable.Drawable getBottomDrawable();
+    method public final android.graphics.Bitmap getCoverBitmap();
+    method public final android.graphics.drawable.Drawable getCoverDrawable();
+    method public final int getParallaxDrawableMaxOffset();
+    method public final int getSolidColor();
+    method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+    method public android.support.v4.app.Fragment onCreateVideoSupportFragment();
+    method public final void setCoverBitmap(android.graphics.Bitmap);
+    method public final void setParallaxDrawableMaxOffset(int);
+    method public final void setSolidColor(int);
+    method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+  }
+
   public class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
     ctor public ErrorFragment();
     method public android.graphics.drawable.Drawable getBackgroundDrawable();
@@ -2040,6 +2120,7 @@
     method public void setFadingEnabled(boolean);
     method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
     method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+    method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
     method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
     method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
     method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
@@ -2124,6 +2205,7 @@
     method public void setFadingEnabled(boolean);
     method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
     method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+    method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
     method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
     method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
     method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
@@ -2212,6 +2294,7 @@
     method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
     method public android.graphics.drawable.Drawable getBadgeDrawable();
     method public android.content.Intent getRecognizerIntent();
+    method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
     method public java.lang.String getTitle();
     method public static android.support.v17.leanback.app.SearchFragment newInstance(java.lang.String);
     method public void setBadgeDrawable(android.graphics.drawable.Drawable);
@@ -2241,6 +2324,7 @@
     method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
     method public android.graphics.drawable.Drawable getBadgeDrawable();
     method public android.content.Intent getRecognizerIntent();
+    method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
     method public java.lang.String getTitle();
     method public static android.support.v17.leanback.app.SearchSupportFragment newInstance(java.lang.String);
     method public void setBadgeDrawable(android.graphics.drawable.Drawable);
@@ -2290,6 +2374,28 @@
     method public void setSelectedPosition(int);
   }
 
+  public class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
+    ctor public VideoFragment();
+    method public android.view.SurfaceView getSurfaceView();
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+    ctor public VideoFragmentGlueHost(android.support.v17.leanback.app.VideoFragment);
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoSupportFragment extends android.support.v17.leanback.app.PlaybackSupportFragment {
+    ctor public VideoSupportFragment();
+    method public android.view.SurfaceView getSurfaceView();
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
+  public class VideoSupportFragmentGlueHost extends android.support.v17.leanback.app.PlaybackSupportFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+    ctor public VideoSupportFragmentGlueHost(android.support.v17.leanback.app.VideoSupportFragment);
+    method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+  }
+
 }
 
 package android.support.v17.leanback.database {
@@ -2305,6 +2411,26 @@
 
 package android.support.v17.leanback.graphics {
 
+  public class BoundsRule {
+    ctor public BoundsRule();
+    ctor public BoundsRule(android.support.v17.leanback.graphics.BoundsRule);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule absoluteValue(int);
+    method public void calculateBounds(android.graphics.Rect, android.graphics.Rect);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParent(float);
+    method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParentWithOffset(float, int);
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule bottom;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule left;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule right;
+    field public android.support.v17.leanback.graphics.BoundsRule.ValueRule top;
+  }
+
+  public static final class BoundsRule.ValueRule {
+    method public int getAbsoluteValue();
+    method public float getFraction();
+    method public void setAbsoluteValue(int);
+    method public void setFraction(float);
+  }
+
   public final class ColorFilterCache {
     method public static android.support.v17.leanback.graphics.ColorFilterCache getColorFilterCache(int);
     method public android.graphics.ColorFilter getFilterForLevel(float);
@@ -2331,6 +2457,54 @@
     method public void setActiveLevel(float);
   }
 
+  public class CompositeDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
+    ctor public CompositeDrawable();
+    method public void addChildDrawable(android.graphics.drawable.Drawable);
+    method public void draw(android.graphics.Canvas);
+    method public android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable getChildAt(int);
+    method public int getChildCount();
+    method public android.graphics.drawable.Drawable getDrawable(int);
+    method public int getOpacity();
+    method public void invalidateDrawable(android.graphics.drawable.Drawable);
+    method public void removeChild(int);
+    method public void removeDrawable(android.graphics.drawable.Drawable);
+    method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long);
+    method public void setAlpha(int);
+    method public void setChildDrawableAt(int, android.graphics.drawable.Drawable);
+    method public void setColorFilter(android.graphics.ColorFilter);
+    method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
+  }
+
+  public static final class CompositeDrawable.ChildDrawable {
+    ctor public CompositeDrawable.ChildDrawable(android.graphics.drawable.Drawable, android.support.v17.leanback.graphics.CompositeDrawable);
+    method public android.support.v17.leanback.graphics.BoundsRule getBoundsRule();
+    method public android.graphics.drawable.Drawable getDrawable();
+    method public void recomputeBounds();
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> BOTTOM_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> BOTTOM_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> LEFT_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> LEFT_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> RIGHT_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> RIGHT_FRACTION;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> TOP_ABSOLUTE;
+    field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> TOP_FRACTION;
+  }
+
+  public class FitWidthBitmapDrawable extends android.graphics.drawable.Drawable {
+    ctor public FitWidthBitmapDrawable();
+    method public void draw(android.graphics.Canvas);
+    method public android.graphics.Bitmap getBitmap();
+    method public int getOpacity();
+    method public android.graphics.Rect getSource();
+    method public int getVerticalOffset();
+    method public void setAlpha(int);
+    method public void setBitmap(android.graphics.Bitmap);
+    method public void setColorFilter(android.graphics.ColorFilter);
+    method public void setSource(android.graphics.Rect);
+    method public void setVerticalOffset(int);
+    field public static final android.util.Property<android.support.v17.leanback.graphics.FitWidthBitmapDrawable, java.lang.Integer> PROPERTY_VERTICAL_OFFSET;
+  }
+
 }
 
 package android.support.v17.leanback.media {
@@ -2438,6 +2612,7 @@
 
   public static abstract class PlaybackGlueHost.HostCallback {
     ctor public PlaybackGlueHost.HostCallback();
+    method public void onHostDestroy();
     method public void onHostPause();
     method public void onHostResume();
     method public void onHostStart();
@@ -2727,6 +2902,12 @@
     field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDetailsDescriptionViewHolder;
   }
 
+  public class DetailsParallax extends android.support.v17.leanback.widget.RecyclerViewParallax {
+    ctor public DetailsParallax();
+    method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowBottom();
+    method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowTop();
+  }
+
   public class DividerPresenter extends android.support.v17.leanback.widget.Presenter {
     ctor public DividerPresenter();
     method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
@@ -2759,6 +2940,7 @@
     ctor public FocusHighlightHelper();
     method public static void setupBrowseItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter, int, boolean);
     method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView);
+    method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView, boolean);
   }
 
   public abstract interface FragmentAnimationProvider {
@@ -3160,6 +3342,7 @@
     method public final void onViewRecycled(android.support.v7.widget.RecyclerView.ViewHolder);
     method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
     method public void setAdapterListener(android.support.v17.leanback.widget.ItemBridgeAdapter.AdapterListener);
+    method public void setPresenter(android.support.v17.leanback.widget.PresenterSelector);
     method public void setPresenterMapper(java.util.ArrayList<android.support.v17.leanback.widget.Presenter>);
     method public void setWrapper(android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper);
   }
@@ -3347,6 +3530,115 @@
     method public final boolean isRenderedAsRowView();
   }
 
+  public abstract class Parallax<PropertyT extends android.util.Property> {
+    ctor public Parallax();
+    method public void addEffect(android.support.v17.leanback.widget.ParallaxEffect);
+    method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue...);
+    method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue...);
+    method public abstract PropertyT addProperty(java.lang.String);
+    method public abstract PropertyT createProperty(java.lang.String, int);
+    method public java.util.List<android.support.v17.leanback.widget.ParallaxEffect> getEffects();
+    method public final java.util.List<PropertyT> getProperties();
+    method public void removeAllEffects();
+    method public void removeEffect(android.support.v17.leanback.widget.ParallaxEffect);
+    method public void updateValues();
+    method public abstract void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static abstract class Parallax.FloatParallax<FloatPropertyT extends android.support.v17.leanback.widget.Parallax.FloatProperty> extends android.support.v17.leanback.widget.Parallax {
+    ctor public Parallax.FloatParallax();
+    method public final FloatPropertyT addProperty(java.lang.String);
+    method public abstract float getMaxValue();
+    method public final float getPropertyValue(int);
+    method public final void setPropertyValue(int, float);
+    method public final void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static class Parallax.FloatProperty extends android.util.Property {
+    ctor public Parallax.FloatProperty(java.lang.String, int);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue at(float, float);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue atAbsolute(float);
+    method public final android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue atFraction(float);
+    method public final java.lang.Float get(android.support.v17.leanback.widget.Parallax.FloatParallax);
+    method public final int getIndex();
+    method public final void set(android.support.v17.leanback.widget.Parallax.FloatParallax, java.lang.Float);
+    field public static final float UNKNOWN_AFTER = 3.4028235E38f;
+    field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
+  }
+
+  public static class Parallax.FloatPropertyMarkerValue extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue {
+    ctor public Parallax.FloatPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.FloatProperty, float);
+    ctor public Parallax.FloatPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.FloatProperty, float, float);
+    method public final float getMarkerValue(android.support.v17.leanback.widget.Parallax.FloatParallax);
+  }
+
+  public static abstract class Parallax.IntParallax<IntPropertyT extends android.support.v17.leanback.widget.Parallax.IntProperty> extends android.support.v17.leanback.widget.Parallax {
+    ctor public Parallax.IntParallax();
+    method public final IntPropertyT addProperty(java.lang.String);
+    method public abstract int getMaxValue();
+    method public final int getPropertyValue(int);
+    method public final void setPropertyValue(int, int);
+    method public final void verifyProperties() throws java.lang.IllegalStateException;
+  }
+
+  public static class Parallax.IntProperty extends android.util.Property {
+    ctor public Parallax.IntProperty(java.lang.String, int);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue at(int, float);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue atAbsolute(int);
+    method public final android.support.v17.leanback.widget.Parallax.IntPropertyMarkerValue atFraction(float);
+    method public final java.lang.Integer get(android.support.v17.leanback.widget.Parallax.IntParallax);
+    method public final int getIndex();
+    method public final void set(android.support.v17.leanback.widget.Parallax.IntParallax, java.lang.Integer);
+    field public static final int UNKNOWN_AFTER = 2147483647; // 0x7fffffff
+    field public static final int UNKNOWN_BEFORE = -2147483648; // 0x80000000
+  }
+
+  public static class Parallax.IntPropertyMarkerValue extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue {
+    ctor public Parallax.IntPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.IntProperty, int);
+    ctor public Parallax.IntPropertyMarkerValue(android.support.v17.leanback.widget.Parallax.IntProperty, int, float);
+    method public final int getMarkerValue(android.support.v17.leanback.widget.Parallax.IntParallax);
+  }
+
+  public static class Parallax.PropertyMarkerValue<PropertyT> {
+    ctor public Parallax.PropertyMarkerValue(PropertyT);
+    method public PropertyT getProperty();
+  }
+
+  public abstract class ParallaxEffect<ParallaxEffectT extends android.support.v17.leanback.widget.ParallaxEffect, PropertyMarkerValueT extends android.support.v17.leanback.widget.Parallax.PropertyMarkerValue> {
+    ctor public ParallaxEffect();
+    method public final void addTarget(android.support.v17.leanback.widget.ParallaxTarget);
+    method protected abstract float calculateFraction(android.support.v17.leanback.widget.Parallax);
+    method public final java.util.List<PropertyMarkerValueT> getPropertyRanges();
+    method public final java.util.List<android.support.v17.leanback.widget.ParallaxTarget> getTargets();
+    method public final void performMapping(android.support.v17.leanback.widget.Parallax);
+    method public final void removeTarget(android.support.v17.leanback.widget.ParallaxTarget);
+    method public final void setPropertyRanges(PropertyMarkerValueT...);
+    method public final android.support.v17.leanback.widget.ParallaxEffect target(android.support.v17.leanback.widget.ParallaxTarget);
+    method public final android.support.v17.leanback.widget.ParallaxEffect target(java.lang.Object, android.animation.PropertyValuesHolder);
+  }
+
+  public static final class ParallaxEffect.FloatEffect extends android.support.v17.leanback.widget.ParallaxEffect {
+    ctor public ParallaxEffect.FloatEffect();
+    method protected float calculateFraction(android.support.v17.leanback.widget.Parallax);
+  }
+
+  public static final class ParallaxEffect.IntEffect extends android.support.v17.leanback.widget.ParallaxEffect {
+    ctor public ParallaxEffect.IntEffect();
+    method protected float calculateFraction(android.support.v17.leanback.widget.Parallax);
+  }
+
+  public abstract class ParallaxTarget {
+    ctor public ParallaxTarget();
+    method public abstract float getFraction();
+    method public abstract void update(float);
+  }
+
+  public static final class ParallaxTarget.PropertyValuesHolderTarget extends android.support.v17.leanback.widget.ParallaxTarget {
+    ctor public ParallaxTarget.PropertyValuesHolderTarget(java.lang.Object, android.animation.PropertyValuesHolder);
+    method public float getFraction();
+    method public void update(float);
+  }
+
   public class PlaybackControlsRow extends android.support.v17.leanback.widget.Row {
     ctor public PlaybackControlsRow(java.lang.Object);
     ctor public PlaybackControlsRow();
@@ -3536,6 +3828,25 @@
     method public void unselect();
   }
 
+  public class RecyclerViewParallax extends android.support.v17.leanback.widget.Parallax.IntParallax {
+    ctor public RecyclerViewParallax();
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty createProperty(java.lang.String, int);
+    method public int getMaxValue();
+    method public android.support.v7.widget.RecyclerView getRecyclerView();
+    method public void setRecyclerView(android.support.v7.widget.RecyclerView);
+  }
+
+  public static final class RecyclerViewParallax.ChildPositionProperty extends android.support.v17.leanback.widget.Parallax.IntProperty {
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty adapterPosition(int);
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty fraction(float);
+    method public int getAdapterPosition();
+    method public float getFraction();
+    method public int getOffset();
+    method public int getViewId();
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty offset(int);
+    method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty viewId(int);
+  }
+
   public class Row {
     ctor public Row(long, android.support.v17.leanback.widget.HeaderItem);
     ctor public Row(android.support.v17.leanback.widget.HeaderItem);
@@ -3957,6 +4268,18 @@
     method public void setStaticLabels(java.lang.CharSequence[]);
   }
 
+  public class TimePicker extends android.support.v17.leanback.widget.picker.Picker {
+    ctor public TimePicker(android.content.Context, android.util.AttributeSet);
+    ctor public TimePicker(android.content.Context, android.util.AttributeSet, int);
+    method public int getHour();
+    method public int getMinute();
+    method public boolean is24Hour();
+    method public boolean isPm();
+    method public void setHour(int);
+    method public void setIs24Hour(boolean);
+    method public void setMinute(int);
+  }
+
 }
 
 package android.support.v17.preference {
@@ -5264,6 +5587,10 @@
     method public static int setAlphaComponent(int, int);
   }
 
+  public final class PaintCompat {
+    method public static boolean hasGlyph(android.graphics.Paint, java.lang.String);
+  }
+
 }
 
 package android.support.v4.graphics.drawable {
@@ -5376,6 +5703,7 @@
     method public android.content.ComponentName getServiceComponent();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
     method public boolean isConnected();
+    method public void search(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SearchCallback);
     method public void subscribe(java.lang.String, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
     method public void subscribe(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
     method public void unsubscribe(java.lang.String);
@@ -5413,6 +5741,12 @@
     field public static final int FLAG_PLAYABLE = 2; // 0x2
   }
 
+  public static abstract class MediaBrowserCompat.SearchCallback {
+    ctor public MediaBrowserCompat.SearchCallback();
+    method public void onError(java.lang.String, android.os.Bundle);
+    method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+  }
+
   public static abstract class MediaBrowserCompat.SubscriptionCallback {
     ctor public MediaBrowserCompat.SubscriptionCallback();
     method public void onChildrenLoaded(java.lang.String, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
@@ -5433,6 +5767,7 @@
     method public abstract void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
     method public void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>, android.os.Bundle);
     method public void onLoadItem(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+    method public void onSearch(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
     method public void setSessionToken(android.support.v4.media.session.MediaSessionCompat.Token);
     field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
   }
@@ -5444,7 +5779,7 @@
     field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
     field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
     field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
-    field public static final java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
+    field public static final deprecated java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
   }
 
   public static class MediaBrowserServiceCompat.Result<T> {
@@ -5505,6 +5840,7 @@
     method public int size();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.support.v4.media.MediaMetadataCompat> CREATOR;
+    field public static final java.lang.String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
     field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
     field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
@@ -5573,72 +5909,72 @@
     field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2
   }
 
-  public abstract class TransportController {
-    ctor public TransportController();
-    method public abstract int getBufferPercentage();
-    method public abstract long getCurrentPosition();
-    method public abstract long getDuration();
-    method public abstract int getTransportControlFlags();
-    method public abstract boolean isPlaying();
-    method public abstract void pausePlaying();
-    method public abstract void registerStateListener(android.support.v4.media.TransportStateListener);
-    method public abstract void seekTo(long);
-    method public abstract void startPlaying();
-    method public abstract void stopPlaying();
-    method public abstract void unregisterStateListener(android.support.v4.media.TransportStateListener);
+  public abstract deprecated class TransportController {
+    ctor public deprecated TransportController();
+    method public abstract deprecated int getBufferPercentage();
+    method public abstract deprecated long getCurrentPosition();
+    method public abstract deprecated long getDuration();
+    method public abstract deprecated int getTransportControlFlags();
+    method public abstract deprecated boolean isPlaying();
+    method public abstract deprecated void pausePlaying();
+    method public abstract deprecated void registerStateListener(android.support.v4.media.TransportStateListener);
+    method public abstract deprecated void seekTo(long);
+    method public abstract deprecated void startPlaying();
+    method public abstract deprecated void stopPlaying();
+    method public abstract deprecated void unregisterStateListener(android.support.v4.media.TransportStateListener);
   }
 
-  public class TransportMediator extends android.support.v4.media.TransportController {
-    ctor public TransportMediator(android.app.Activity, android.support.v4.media.TransportPerformer);
-    ctor public TransportMediator(android.view.View, android.support.v4.media.TransportPerformer);
-    method public void destroy();
-    method public boolean dispatchKeyEvent(android.view.KeyEvent);
-    method public int getBufferPercentage();
-    method public long getCurrentPosition();
-    method public long getDuration();
-    method public java.lang.Object getRemoteControlClient();
-    method public int getTransportControlFlags();
-    method public boolean isPlaying();
-    method public void pausePlaying();
-    method public void refreshState();
-    method public void registerStateListener(android.support.v4.media.TransportStateListener);
-    method public void seekTo(long);
-    method public void startPlaying();
-    method public void stopPlaying();
-    method public void unregisterStateListener(android.support.v4.media.TransportStateListener);
-    field public static final int FLAG_KEY_MEDIA_FAST_FORWARD = 64; // 0x40
-    field public static final int FLAG_KEY_MEDIA_NEXT = 128; // 0x80
-    field public static final int FLAG_KEY_MEDIA_PAUSE = 16; // 0x10
-    field public static final int FLAG_KEY_MEDIA_PLAY = 4; // 0x4
-    field public static final int FLAG_KEY_MEDIA_PLAY_PAUSE = 8; // 0x8
-    field public static final int FLAG_KEY_MEDIA_PREVIOUS = 1; // 0x1
-    field public static final int FLAG_KEY_MEDIA_REWIND = 2; // 0x2
-    field public static final int FLAG_KEY_MEDIA_STOP = 32; // 0x20
-    field public static final int KEYCODE_MEDIA_PAUSE = 127; // 0x7f
-    field public static final int KEYCODE_MEDIA_PLAY = 126; // 0x7e
-    field public static final int KEYCODE_MEDIA_RECORD = 130; // 0x82
+  public deprecated class TransportMediator extends android.support.v4.media.TransportController {
+    ctor public deprecated TransportMediator(android.app.Activity, android.support.v4.media.TransportPerformer);
+    ctor public deprecated TransportMediator(android.view.View, android.support.v4.media.TransportPerformer);
+    method public deprecated void destroy();
+    method public deprecated boolean dispatchKeyEvent(android.view.KeyEvent);
+    method public deprecated int getBufferPercentage();
+    method public deprecated long getCurrentPosition();
+    method public deprecated long getDuration();
+    method public deprecated java.lang.Object getRemoteControlClient();
+    method public deprecated int getTransportControlFlags();
+    method public deprecated boolean isPlaying();
+    method public deprecated void pausePlaying();
+    method public deprecated void refreshState();
+    method public deprecated void registerStateListener(android.support.v4.media.TransportStateListener);
+    method public deprecated void seekTo(long);
+    method public deprecated void startPlaying();
+    method public deprecated void stopPlaying();
+    method public deprecated void unregisterStateListener(android.support.v4.media.TransportStateListener);
+    field public static final deprecated int FLAG_KEY_MEDIA_FAST_FORWARD = 64; // 0x40
+    field public static final deprecated int FLAG_KEY_MEDIA_NEXT = 128; // 0x80
+    field public static final deprecated int FLAG_KEY_MEDIA_PAUSE = 16; // 0x10
+    field public static final deprecated int FLAG_KEY_MEDIA_PLAY = 4; // 0x4
+    field public static final deprecated int FLAG_KEY_MEDIA_PLAY_PAUSE = 8; // 0x8
+    field public static final deprecated int FLAG_KEY_MEDIA_PREVIOUS = 1; // 0x1
+    field public static final deprecated int FLAG_KEY_MEDIA_REWIND = 2; // 0x2
+    field public static final deprecated int FLAG_KEY_MEDIA_STOP = 32; // 0x20
+    field public static final deprecated int KEYCODE_MEDIA_PAUSE = 127; // 0x7f
+    field public static final deprecated int KEYCODE_MEDIA_PLAY = 126; // 0x7e
+    field public static final deprecated int KEYCODE_MEDIA_RECORD = 130; // 0x82
   }
 
-  public abstract class TransportPerformer {
-    ctor public TransportPerformer();
-    method public void onAudioFocusChange(int);
-    method public int onGetBufferPercentage();
-    method public abstract long onGetCurrentPosition();
-    method public abstract long onGetDuration();
-    method public int onGetTransportControlFlags();
-    method public abstract boolean onIsPlaying();
-    method public boolean onMediaButtonDown(int, android.view.KeyEvent);
-    method public boolean onMediaButtonUp(int, android.view.KeyEvent);
-    method public abstract void onPause();
-    method public abstract void onSeekTo(long);
-    method public abstract void onStart();
-    method public abstract void onStop();
+  public abstract deprecated class TransportPerformer {
+    ctor public deprecated TransportPerformer();
+    method public deprecated void onAudioFocusChange(int);
+    method public deprecated int onGetBufferPercentage();
+    method public abstract deprecated long onGetCurrentPosition();
+    method public abstract deprecated long onGetDuration();
+    method public deprecated int onGetTransportControlFlags();
+    method public abstract deprecated boolean onIsPlaying();
+    method public deprecated boolean onMediaButtonDown(int, android.view.KeyEvent);
+    method public deprecated boolean onMediaButtonUp(int, android.view.KeyEvent);
+    method public abstract deprecated void onPause();
+    method public abstract deprecated void onSeekTo(long);
+    method public abstract deprecated void onStart();
+    method public abstract deprecated void onStop();
   }
 
-  public class TransportStateListener {
-    ctor public TransportStateListener();
-    method public void onPlayingChanged(android.support.v4.media.TransportController);
-    method public void onTransportControlsChanged(android.support.v4.media.TransportController);
+  public deprecated class TransportStateListener {
+    ctor public deprecated TransportStateListener();
+    method public deprecated void onPlayingChanged(android.support.v4.media.TransportController);
+    method public deprecated void onTransportControlsChanged(android.support.v4.media.TransportController);
   }
 
   public abstract class VolumeProviderCompat {
@@ -5676,6 +6012,8 @@
   public final class MediaControllerCompat {
     ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat);
     ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat.Token) throws android.os.RemoteException;
+    method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat);
+    method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat, int);
     method public void adjustVolume(int, int);
     method public boolean dispatchMediaButtonEvent(android.view.KeyEvent);
     method public android.os.Bundle getExtras();
@@ -5689,11 +6027,15 @@
     method public java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue();
     method public java.lang.CharSequence getQueueTitle();
     method public int getRatingType();
+    method public int getRepeatMode();
     method public android.app.PendingIntent getSessionActivity();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
     method public android.support.v4.media.session.MediaControllerCompat.TransportControls getTransportControls();
+    method public boolean isShuffleModeEnabled();
     method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback);
     method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback, android.os.Handler);
+    method public void removeQueueItem(android.support.v4.media.MediaDescriptionCompat);
+    method public void removeQueueItemAt(int);
     method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public static void setMediaController(android.app.Activity, android.support.v4.media.session.MediaControllerCompat);
     method public void setVolumeTo(int, int);
@@ -5709,8 +6051,10 @@
     method public void onPlaybackStateChanged(android.support.v4.media.session.PlaybackStateCompat);
     method public void onQueueChanged(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
     method public void onQueueTitleChanged(java.lang.CharSequence);
+    method public void onRepeatModeChanged(int);
     method public void onSessionDestroyed();
     method public void onSessionEvent(java.lang.String, android.os.Bundle);
+    method public void onShuffleModeChanged(boolean);
   }
 
   public static final class MediaControllerCompat.PlaybackInfo {
@@ -5739,6 +6083,8 @@
     method public abstract void sendCustomAction(android.support.v4.media.session.PlaybackStateCompat.CustomAction, android.os.Bundle);
     method public abstract void sendCustomAction(java.lang.String, android.os.Bundle);
     method public abstract void setRating(android.support.v4.media.RatingCompat);
+    method public abstract void setRepeatMode(int);
+    method public abstract void setShuffleModeEnabled(boolean);
     method public abstract void skipToNext();
     method public abstract void skipToPrevious();
     method public abstract void skipToQueueItem(long);
@@ -5772,13 +6118,18 @@
     method public void setQueue(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
     method public void setQueueTitle(java.lang.CharSequence);
     method public void setRatingType(int);
+    method public void setRepeatMode(int);
     method public void setSessionActivity(android.app.PendingIntent);
+    method public void setShuffleModeEnabled(boolean);
     field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
+    field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4
     field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
   }
 
   public static abstract class MediaSessionCompat.Callback {
     ctor public MediaSessionCompat.Callback();
+    method public void onAddQueueItem(android.support.v4.media.MediaDescriptionCompat);
+    method public void onAddQueueItem(android.support.v4.media.MediaDescriptionCompat, int);
     method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public void onCustomAction(java.lang.String, android.os.Bundle);
     method public void onFastForward();
@@ -5792,9 +6143,13 @@
     method public void onPrepareFromMediaId(java.lang.String, android.os.Bundle);
     method public void onPrepareFromSearch(java.lang.String, android.os.Bundle);
     method public void onPrepareFromUri(android.net.Uri, android.os.Bundle);
+    method public void onRemoveQueueItem(android.support.v4.media.MediaDescriptionCompat);
+    method public void onRemoveQueueItemAt(int);
     method public void onRewind();
     method public void onSeekTo(long);
     method public void onSetRating(android.support.v4.media.RatingCompat);
+    method public void onSetRepeatMode(int);
+    method public void onSetShuffleModeEnabled(boolean);
     method public void onSkipToNext();
     method public void onSkipToPrevious();
     method public void onSkipToQueueItem(long);
@@ -5847,6 +6202,7 @@
     method public long getActiveQueueItemId();
     method public long getBufferedPosition();
     method public java.util.List<android.support.v4.media.session.PlaybackStateCompat.CustomAction> getCustomActions();
+    method public int getErrorCode();
     method public java.lang.CharSequence getErrorMessage();
     method public android.os.Bundle getExtras();
     method public long getLastPositionUpdateTime();
@@ -5870,12 +6226,29 @@
     field public static final long ACTION_REWIND = 8L; // 0x8L
     field public static final long ACTION_SEEK_TO = 256L; // 0x100L
     field public static final long ACTION_SET_RATING = 128L; // 0x80L
+    field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
+    field public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 524288L; // 0x80000L
     field public static final long ACTION_SKIP_TO_NEXT = 32L; // 0x20L
     field public static final long ACTION_SKIP_TO_PREVIOUS = 16L; // 0x10L
     field public static final long ACTION_SKIP_TO_QUEUE_ITEM = 4096L; // 0x1000L
     field public static final long ACTION_STOP = 1L; // 0x1L
     field public static final android.os.Parcelable.Creator<android.support.v4.media.session.PlaybackStateCompat> CREATOR;
+    field public static final int ERROR_CODE_ACTION_ABORTED = 10; // 0xa
+    field public static final int ERROR_CODE_APP_ERROR = 1; // 0x1
+    field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3; // 0x3
+    field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5; // 0x5
+    field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8; // 0x8
+    field public static final int ERROR_CODE_END_OF_QUEUE = 11; // 0xb
+    field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7; // 0x7
+    field public static final int ERROR_CODE_NOT_SUPPORTED = 2; // 0x2
+    field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6; // 0x6
+    field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4; // 0x4
+    field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; // 0x9
+    field public static final int ERROR_CODE_UNKNOWN_ERROR = 0; // 0x0
     field public static final long PLAYBACK_POSITION_UNKNOWN = -1L; // 0xffffffffffffffffL
+    field public static final int REPEAT_MODE_ALL = 2; // 0x2
+    field public static final int REPEAT_MODE_NONE = 0; // 0x0
+    field public static final int REPEAT_MODE_ONE = 1; // 0x1
     field public static final int STATE_BUFFERING = 6; // 0x6
     field public static final int STATE_CONNECTING = 8; // 0x8
     field public static final int STATE_ERROR = 7; // 0x7
@@ -5899,7 +6272,8 @@
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setActions(long);
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setActiveQueueItemId(long);
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setBufferedPosition(long);
-    method public android.support.v4.media.session.PlaybackStateCompat.Builder setErrorMessage(java.lang.CharSequence);
+    method public deprecated android.support.v4.media.session.PlaybackStateCompat.Builder setErrorMessage(java.lang.CharSequence);
+    method public android.support.v4.media.session.PlaybackStateCompat.Builder setErrorMessage(int, java.lang.CharSequence);
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setExtras(android.os.Bundle);
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setState(int, long, float);
     method public android.support.v4.media.session.PlaybackStateCompat.Builder setState(int, long, float, long);
@@ -8098,6 +8472,7 @@
     method public android.support.v7.graphics.drawable.DrawerArrowDrawable getDrawerArrowDrawable();
     method public android.view.View.OnClickListener getToolbarNavigationClickListener();
     method public boolean isDrawerIndicatorEnabled();
+    method public boolean isDrawerSlideAnimationEnabled();
     method public void onConfigurationChanged(android.content.res.Configuration);
     method public void onDrawerClosed(android.view.View);
     method public void onDrawerOpened(android.view.View);
@@ -8106,6 +8481,7 @@
     method public boolean onOptionsItemSelected(android.view.MenuItem);
     method public void setDrawerArrowDrawable(android.support.v7.graphics.drawable.DrawerArrowDrawable);
     method public void setDrawerIndicatorEnabled(boolean);
+    method public void setDrawerSlideAnimationEnabled(boolean);
     method public void setHomeAsUpIndicator(android.graphics.drawable.Drawable);
     method public void setHomeAsUpIndicator(int);
     method public void setToolbarNavigationClickListener(android.view.View.OnClickListener);
@@ -10229,6 +10605,7 @@
     method public boolean isLayoutHierarchical(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
     method public boolean isMeasurementCacheEnabled();
     method public boolean isSmoothScrolling();
+    method public boolean isViewPartiallyVisible(android.view.View, boolean, boolean);
     method public void layoutDecorated(android.view.View, int, int, int, int);
     method public void layoutDecoratedWithMargins(android.view.View, int, int, int, int);
     method public void measureChild(android.view.View, int, int);
@@ -10273,6 +10650,7 @@
     method public void removeView(android.view.View);
     method public void removeViewAt(int);
     method public boolean requestChildRectangleOnScreen(android.support.v7.widget.RecyclerView, android.view.View, android.graphics.Rect, boolean);
+    method public boolean requestChildRectangleOnScreen(android.support.v7.widget.RecyclerView, android.view.View, android.graphics.Rect, boolean, boolean);
     method public void requestLayout();
     method public void requestSimpleAnimationsInNextLayout();
     method public int scrollHorizontallyBy(int, android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
diff --git a/build.gradle b/build.gradle
index 28d5849..6e593d7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,7 +24,7 @@
     }
     dependencies {
         // Keep gradle plugin version in sync with ub_supportlib-master manifest.
-        classpath 'com.android.tools.build:gradle:2.2.1'
+        classpath 'com.android.tools.build:gradle:2.2.4'
     }
 }
 
@@ -40,11 +40,12 @@
     doclava project(':doclava')
 }
 
-ext.supportVersion = '25.2.0'
+ext.supportVersion = '25.3.0-SNAPSHOT'
 ext.extraVersion = 44
 ext.supportRepoOut = ''
 ext.buildNumber = Integer.toString(ext.extraVersion)
 
+ext.buildToolsVersion = '25.0.0'
 ext.testRunnerVersion = '0.6-alpha'
 ext.espressoVersion = '2.3-alpha'
 
@@ -52,20 +53,21 @@
 // required for the doclava dependency.
 ext.usePrebuilts = "true"
 
+// Prevent the Android Gradle plug-in from automatically downloading SDK dependencies.
+ext['android.builder.sdkDownload'] = false
+
 final String platform = OperatingSystem.current().isMacOsX() ? 'darwin' : 'linux'
 System.setProperty('android.dir', "${rootDir}/../../")
 final String fullSdkPath = "${rootDir}/../../prebuilts/fullsdk-${platform}"
 if (file(fullSdkPath).exists()) {
     gradle.ext.currentSdk = 25
-    ext.buildToolsVersion = '24.0.1'
     project.ext.androidJar = files("${fullSdkPath}/platforms/android-${gradle.ext.currentSdk}/android.jar")
     System.setProperty('android.home', "${rootDir}/../../prebuilts/fullsdk-${platform}")
     File props = file("local.properties")
     props.write "sdk.dir=${fullSdkPath}"
 } else {
-    gradle.ext.currentSdk = 25
-    ext.buildToolsVersion = '24.0.1'
-    project.ext.androidJar = files("${project.rootDir}/../../prebuilts/sdk/${gradle.ext.currentSdk}/android.jar")
+    gradle.ext.currentSdk = 'current'
+    project.ext.androidJar = files("${project.rootDir}/../../prebuilts/sdk/current/android.jar")
     File props = file("local.properties")
     props.write "android.dir=../../"
 }
@@ -163,6 +165,7 @@
     Files.write(xml, new File(project.ext.distDir, 'repo-extras.xml'), Charsets.UTF_8)
 }
 createArchive.dependsOn createXml
+createXml.dependsOn createRepository
 
 task(createSourceProp) << {
     def sourceProp =
@@ -208,6 +211,9 @@
 
 // Generates online docs.
 task generateDocs(type: DoclavaTask, dependsOn: configurations.doclava) {
+    group = JavaBasePlugin.DOCUMENTATION_GROUP
+    description = 'Generates d.android.com style documentation.'
+
     docletpath = configurations.doclava.resolve()
     destinationDir = new File(project.docsDir, "online")
 
@@ -263,6 +269,9 @@
 
 // Copies generated API files to current version.
 task updateApi(type: UpdateApiTask, dependsOn: generateApi) {
+    group JavaBasePlugin.VERIFICATION_GROUP
+    description 'Invoke Doclava\'s ApiCheck tool to update current.txt based on current changes.'
+
     newApiFile = new File(project.docsDir, 'release/current.txt')
     oldApiFile = new File(project.rootDir, 'api/current.txt')
     newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
@@ -433,7 +442,7 @@
     }
 
     project.afterEvaluate {
-        // The archivesBaseName isn't available intially, so set it now
+        // The archivesBaseName isn't available initially, so set it now
         def createZipTask = project.tasks.findByName("createSeparateZip")
         if (createZipTask != null) {
             createZipTask.appendix = archivesBaseName
@@ -488,6 +497,14 @@
             }
         }
     }
+
+    // Update the version meta-data in each Manifest
+    project.afterEvaluate { p ->
+        if (p.hasProperty('android')) {
+            p.android.defaultConfig.manifestPlaceholders =
+                    ["support-version": rootProject.ext.supportVersion]
+        }
+    }
 }
 
 project.gradle.buildFinished { buildResult ->
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
index 3578958..c245f77 100644
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
@@ -198,6 +198,7 @@
 
     public CheckApiTask() {
         group = 'Verification'
+        description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.'
     }
 
     private Set<File> collectAndVerifyInputs() {
diff --git a/compat/Android.mk b/compat/Android.mk
index ba5b958..97916ae 100644
--- a/compat/Android.mk
+++ b/compat/Android.mk
@@ -45,6 +45,7 @@
     $(call all-java-files-under,java) \
     $(call all-Iaidl-files-under,java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations
 LOCAL_JAR_EXCLUDE_FILES := none
diff --git a/compat/AndroidManifest-make.xml b/compat/AndroidManifest-make.xml
new file mode 100644
index 0000000..b2bd5bb
--- /dev/null
+++ b/compat/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.compat">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.compat"/>
+    <application />
+</manifest>
diff --git a/compat/AndroidManifest.xml b/compat/AndroidManifest.xml
index b2bd5bb..55ddcff 100644
--- a/compat/AndroidManifest.xml
+++ b/compat/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.compat">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.compat"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/compat/api23/android/support/v4/graphics/PaintCompatApi23.java b/compat/api23/android/support/v4/graphics/PaintCompatApi23.java
new file mode 100644
index 0000000..c51f175
--- /dev/null
+++ b/compat/api23/android/support/v4/graphics/PaintCompatApi23.java
@@ -0,0 +1,28 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Paint;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+
+@RequiresApi(23)
+class PaintCompatApi23 {
+    static boolean hasGlyph(@NonNull Paint paint, @NonNull String string) {
+        return paint.hasGlyph(string);
+    }
+}
diff --git a/compat/build.gradle b/compat/build.gradle
index 5722998..e87db0e 100644
--- a/compat/build.gradle
+++ b/compat/build.gradle
@@ -15,9 +15,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -58,11 +55,7 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
         compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -73,22 +66,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -96,7 +73,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/compat/gingerbread/android/support/v4/graphics/PaintCompatGingerbread.java b/compat/gingerbread/android/support/v4/graphics/PaintCompatGingerbread.java
new file mode 100644
index 0000000..0d1076f
--- /dev/null
+++ b/compat/gingerbread/android/support/v4/graphics/PaintCompatGingerbread.java
@@ -0,0 +1,101 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.v4.util.Pair;
+
+@RequiresApi(9)
+class PaintCompatGingerbread {
+    // U+DFFFD which is very end of unassigned plane.
+    private static final String TOFU_STRING = "\uDB3F\uDFFD";
+
+    private static final ThreadLocal<Pair<Rect, Rect>> sRectThreadLocal = new ThreadLocal<>();
+
+    static boolean hasGlyph(@NonNull Paint paint, @NonNull String string) {
+        final int length = string.length();
+
+        if (length == 1 && Character.isWhitespace(string.charAt(0))) {
+            // measureText + getTextBounds skips whitespace so we need to special case it here
+            return true;
+        }
+
+        final float missingGlyphWidth = paint.measureText(TOFU_STRING);
+        final float width = paint.measureText(string);
+
+        if (width == 0f) {
+            // If the string width is 0, it can't be rendered
+            return false;
+        }
+
+        if (string.codePointCount(0, string.length()) > 1) {
+            // Heuristic to detect fallback glyphs for ligatures like flags and ZWJ sequences
+            // Return false if string is rendered too widely
+            if (width > 2 * missingGlyphWidth) {
+                return false;
+            }
+
+            // Heuristic to detect fallback glyphs for ligatures like flags and ZWJ sequences (2).
+            // If width is greater than or equal to the sum of width of each code point, it is very
+            // likely that the system is using fallback fonts to draw {@code string} in two or more
+            // glyphs instead of a single ligature glyph. (hasGlyph returns false in this case.)
+            // False detections are possible (the ligature glyph may happen to have the same width
+            // as the sum width), but there are no good way to avoid them.
+            // NOTE: This heuristic does not work with proportional glyphs.
+            // NOTE: This heuristic does not work when a ZWJ sequence is partially combined.
+            // E.g. If system has a glyph for "A ZWJ B" and not for "A ZWJ B ZWJ C", this heuristic
+            // returns true for "A ZWJ B ZWJ C".
+            float sumWidth = 0;
+            int i = 0;
+            while (i < length) {
+                int charCount = Character.charCount(string.codePointAt(i));
+                sumWidth += paint.measureText(string, i, i + charCount);
+                i += charCount;
+            }
+            if (width >= sumWidth) {
+                return false;
+            }
+        }
+
+        if (width != missingGlyphWidth) {
+            // If the widths are different then its not tofu
+            return true;
+        }
+
+        // If the widths are the same, lets check the bounds. The chance of them being
+        // different chars with the same bounds is extremely small
+        final Pair<Rect, Rect> rects = obtainEmptyRects();
+        paint.getTextBounds(TOFU_STRING, 0, TOFU_STRING.length(), rects.first);
+        paint.getTextBounds(string, 0, length, rects.second);
+        return !rects.first.equals(rects.second);
+    }
+
+    private static Pair<Rect, Rect> obtainEmptyRects() {
+        Pair<Rect, Rect> rects = sRectThreadLocal.get();
+        if (rects == null) {
+            rects = new Pair(new Rect(), new Rect());
+            sRectThreadLocal.set(rects);
+        } else {
+            rects.first.setEmpty();
+            rects.second.setEmpty();
+        }
+        return rects;
+    }
+}
diff --git a/compat/java/android/support/v4/app/NotificationCompat.java b/compat/java/android/support/v4/app/NotificationCompat.java
index 492d0fd..5d08191 100644
--- a/compat/java/android/support/v4/app/NotificationCompat.java
+++ b/compat/java/android/support/v4/app/NotificationCompat.java
@@ -17,6 +17,7 @@
 package android.support.v4.app;
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.app.Activity;
 import android.app.Notification;
@@ -30,6 +31,7 @@
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.annotation.ColorInt;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.v4.os.BuildCompat;
@@ -37,6 +39,7 @@
 import android.view.Gravity;
 import android.widget.RemoteViews;
 
+import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -390,6 +393,10 @@
     @ColorInt
     public static final int COLOR_DEFAULT = Color.TRANSPARENT;
 
+    /** @hide */
+    @Retention(SOURCE)
+    @IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
+    public @interface NotificationVisibility {}
     /**
      * Notification visibility: Show this notification in its entirety on all lockscreens.
      *
@@ -1665,7 +1672,7 @@
          *                   {@link Notification#VISIBILITY_PUBLIC}, or
          *                   {@link Notification#VISIBILITY_SECRET}.
          */
-        public Builder setVisibility(int visibility) {
+        public Builder setVisibility(@NotificationVisibility int visibility) {
             mVisibility = visibility;
             return this;
         }
@@ -2147,7 +2154,7 @@
         public static MessagingStyle extractMessagingStyleFromNotification(Notification notif) {
             MessagingStyle style;
             Bundle extras = IMPL.getExtras(notif);
-            if (!extras.containsKey(EXTRA_SELF_DISPLAY_NAME)) {
+            if (extras != null && !extras.containsKey(EXTRA_SELF_DISPLAY_NAME)) {
                 style = null;
             } else {
                 try {
diff --git a/compat/java/android/support/v4/graphics/PaintCompat.java b/compat/java/android/support/v4/graphics/PaintCompat.java
new file mode 100644
index 0000000..66599f7
--- /dev/null
+++ b/compat/java/android/support/v4/graphics/PaintCompat.java
@@ -0,0 +1,44 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Paint;
+import android.os.Build;
+import android.support.annotation.NonNull;
+
+/**
+ * Helper for accessing features in {@link Paint} in a backwards compatible fashion.
+ */
+public final class PaintCompat {
+
+    /**
+     * Determine whether the typeface set on the paint has a glyph supporting the
+     * string in a backwards compatible way.
+     *
+     * @param paint the paint instance to check
+     * @param string the string to test whether there is glyph support
+     * @return true if the typeface set on the given paint has a glyph for the string
+     */
+    public static boolean hasGlyph(@NonNull Paint paint, @NonNull String string) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            return PaintCompatApi23.hasGlyph(paint, string);
+        }
+        return PaintCompatGingerbread.hasGlyph(paint, string);
+    }
+
+    private PaintCompat() {}
+}
diff --git a/compat/jellybean/android/support/v4/app/NotificationCompatJellybean.java b/compat/jellybean/android/support/v4/app/NotificationCompatJellybean.java
index fd873c7..c50e5f0 100644
--- a/compat/jellybean/android/support/v4/app/NotificationCompatJellybean.java
+++ b/compat/jellybean/android/support/v4/app/NotificationCompatJellybean.java
@@ -298,21 +298,24 @@
             RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
         synchronized (sActionsLock) {
             try {
-                Object actionObject = getActionObjectsLocked(notif)[actionIndex];
-                Bundle actionExtras = null;
-                Bundle extras = getExtras(notif);
-                if (extras != null) {
-                    SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray(
-                            EXTRA_ACTION_EXTRAS);
-                    if (actionExtrasMap != null) {
-                        actionExtras = actionExtrasMap.get(actionIndex);
+                Object[] actionObjects = getActionObjectsLocked(notif);
+                if (actionObjects != null) {
+                    Object actionObject = actionObjects[actionIndex];
+                    Bundle actionExtras = null;
+                    Bundle extras = getExtras(notif);
+                    if (extras != null) {
+                        SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray(
+                                EXTRA_ACTION_EXTRAS);
+                        if (actionExtrasMap != null) {
+                            actionExtras = actionExtrasMap.get(actionIndex);
+                        }
                     }
+                    return readAction(factory, remoteInputFactory,
+                            sActionIconField.getInt(actionObject),
+                            (CharSequence) sActionTitleField.get(actionObject),
+                            (PendingIntent) sActionIntentField.get(actionObject),
+                            actionExtras);
                 }
-                return readAction(factory, remoteInputFactory,
-                        sActionIconField.getInt(actionObject),
-                        (CharSequence) sActionTitleField.get(actionObject),
-                        (PendingIntent) sActionIntentField.get(actionObject),
-                        actionExtras);
             } catch (IllegalAccessException e) {
                 Log.e(TAG, "Unable to access notification actions", e);
                 sActionsAccessFailed = true;
diff --git a/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java b/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java
new file mode 100644
index 0000000..26f0691
--- /dev/null
+++ b/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.graphics;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Paint;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+@SmallTest
+public class PaintCompatHasGlyphTest {
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                {"B", true},
+                {"\uDB3F\uDFFD", false},
+                {"Ō", true},
+                {"£", true},
+                {"⅓", true},
+                {"Hello", false},
+                {"\u0020", true},  // white space
+                {"\t\t\t", false},  // more white space
+                {"☺", SDK_INT >= 16}, // glyph added in API 16
+                {"\uD83D\uDC66\uD83C\uDFFF", SDK_INT >= 24}, // glyph added in API 24
+        });
+    }
+
+    private final String mTestString;
+    private final boolean mExpectedResult;
+
+    public PaintCompatHasGlyphTest(String testString, boolean expectedResult) {
+        mTestString = testString;
+        mExpectedResult = expectedResult;
+    }
+
+    @Test
+    public void testHasGlyph() {
+        final boolean hasGlyph = PaintCompat.hasGlyph(new Paint(), mTestString);
+        assertEquals("hasGlyph() returned " + hasGlyph + " for '" + mTestString + "'",
+                mExpectedResult, hasGlyph);
+    }
+}
diff --git a/core-ui/Android.mk b/core-ui/Android.mk
index bdad9f9..eb9acca 100644
--- a/core-ui/Android.mk
+++ b/core-ui/Android.mk
@@ -33,6 +33,7 @@
     $(call all-java-files-under,api21) \
     $(call all-java-files-under,java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-annotations
diff --git a/core-ui/AndroidManifest-make.xml b/core-ui/AndroidManifest-make.xml
new file mode 100644
index 0000000..9bcc44e
--- /dev/null
+++ b/core-ui/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.coreui">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.coreui"/>
+    <application />
+</manifest>
diff --git a/core-ui/AndroidManifest.xml b/core-ui/AndroidManifest.xml
index 9bcc44e..5357112 100644
--- a/core-ui/AndroidManifest.xml
+++ b/core-ui/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.coreui">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.coreui"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index 89796ea..0b94a96 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -16,9 +16,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -54,7 +51,6 @@
 
     testOptions {
         unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
     }
 }
 
@@ -66,22 +62,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -89,7 +69,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/core-utils/Android.mk b/core-utils/Android.mk
index d3f113e..a65a2cd 100644
--- a/core-utils/Android.mk
+++ b/core-utils/Android.mk
@@ -37,6 +37,7 @@
     $(call all-java-files-under,api24) \
     $(call all-java-files-under,java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-annotations
diff --git a/core-utils/AndroidManifest-make.xml b/core-utils/AndroidManifest-make.xml
new file mode 100644
index 0000000..586a28e
--- /dev/null
+++ b/core-utils/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.coreutils">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.coreutils"/>
+    <application />
+</manifest>
diff --git a/core-utils/AndroidManifest.xml b/core-utils/AndroidManifest.xml
index 586a28e..03ff3b4 100644
--- a/core-utils/AndroidManifest.xml
+++ b/core-utils/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.coreutils">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.coreutils"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/core-utils/build.gradle b/core-utils/build.gradle
index 750cf49..d40781d 100644
--- a/core-utils/build.gradle
+++ b/core-utils/build.gradle
@@ -48,11 +48,6 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -63,22 +58,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -86,7 +65,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/customtabs/Android.mk b/customtabs/Android.mk
index cfd9971..e6f6ead 100644
--- a/customtabs/Android.mk
+++ b/customtabs/Android.mk
@@ -31,6 +31,7 @@
     $(call all-java-files-under,src) \
     $(call all-Iaidl-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations \
     android-support-compat
diff --git a/customtabs/AndroidManifest-make.xml b/customtabs/AndroidManifest-make.xml
new file mode 100644
index 0000000..212fab9
--- /dev/null
+++ b/customtabs/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.customtabs">
+    <uses-sdk android:minSdkVersion="15"/>
+    <application />
+</manifest>
diff --git a/customtabs/AndroidManifest.xml b/customtabs/AndroidManifest.xml
index 212fab9..19a1d54 100644
--- a/customtabs/AndroidManifest.xml
+++ b/customtabs/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.customtabs">
     <uses-sdk android:minSdkVersion="15"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/customtabs/build.gradle b/customtabs/build.gradle
index a409dba..bfbeb1a 100644
--- a/customtabs/build.gradle
+++ b/customtabs/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'customtabs'
 
 dependencies {
@@ -32,6 +31,7 @@
 
         androidTest.setRoot('tests')
         androidTest.java.srcDir('tests/src/')
+        androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
     }
 
     compileOptions {
@@ -48,27 +48,10 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsCallback.java b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
index d7fdd39..818118a 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsCallback.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
@@ -19,7 +19,8 @@
 import android.os.Bundle;
 
 /**
- * A callback class for custom tabs client to get messages regarding events in their custom tabs.
+ * A callback class for custom tabs client to get messages regarding events in their custom tabs. In
+ * the implementation, all callbacks are sent to the UI thread for the client.
  */
 public class CustomTabsCallback {
     /**
@@ -76,4 +77,25 @@
      * @param args Arguments for the calback
      */
     public void extraCallback(String callbackName, Bundle args) {}
+
+    /**
+     * Called when {@link CustomTabsSession} has requested a postMessage channel through
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, android.net.Uri)} and the channel
+     * is ready for sending and receiving messages on both ends.
+     *
+     * @param extras Reserved for future use.
+     */
+    public void onMessageChannelReady(Bundle extras) {}
+
+    /**
+     * Called when a tab controlled by this {@link CustomTabsSession} has sent a postMessage.
+     * If postMessage() is called from a single thread, then the messages will be posted in the
+     * same order. When received on the client side, it is the client's responsibility to preserve
+     * the ordering further.
+     *
+     * @param message The message sent.
+     * @param extras Reserved for future use.
+     */
+    public void onPostMessage(String message, Bundle extras) {}
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsClient.java b/customtabs/src/android/support/customtabs/CustomTabsClient.java
index 2d8557b..09f3110 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsClient.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsClient.java
@@ -26,6 +26,8 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
@@ -177,21 +179,60 @@
      * then later with a Custom Tab. The client can then send later service calls or intents to
      * through same session-intent-Custom Tab association.
      * @param callback The callback through which the client will receive updates about the created
-     *                 session. Can be null.
+     *                 session. Can be null. All the callbacks will be received on the UI thread.
      * @return The session object that was created as a result of the transaction. The client can
-     *         use this to relay {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)} calls.
+     *         use this to relay session specific calls.
      *         Null on error.
      */
     public CustomTabsSession newSession(final CustomTabsCallback callback) {
         ICustomTabsCallback.Stub wrapper = new ICustomTabsCallback.Stub() {
+            private Handler mHandler = new Handler(Looper.getMainLooper());
+
             @Override
-            public void onNavigationEvent(int navigationEvent, Bundle extras) {
-                if (callback != null) callback.onNavigationEvent(navigationEvent, extras);
+            public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onNavigationEvent(navigationEvent, extras);
+                    }
+                });
             }
 
             @Override
-            public void extraCallback(String callbackName, Bundle args) throws RemoteException {
-                if (callback != null) callback.extraCallback(callbackName, args);
+            public void extraCallback(final String callbackName, final Bundle args)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.extraCallback(callbackName, args);
+                    }
+                });
+            }
+
+            @Override
+            public void onMessageChannelReady(final Bundle extras)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onMessageChannelReady(extras);
+                    }
+                });
+            }
+
+            @Override
+            public void onPostMessage(final String message, final Bundle extras)
+                    throws RemoteException {
+                if (callback == null) return;
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onPostMessage(message, extras);
+                    }
+                });
             }
         };
 
diff --git a/customtabs/src/android/support/customtabs/CustomTabsService.java b/customtabs/src/android/support/customtabs/CustomTabsService.java
index 25697c5..5a940cf 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsService.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsService.java
@@ -23,8 +23,11 @@
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.support.annotation.IntDef;
 import android.support.v4.util.ArrayMap;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
@@ -35,162 +38,234 @@
  * implementers that want to provide Custom Tabs functionality, not by clients that want to launch
  * Custom Tabs.
  */
- public abstract class CustomTabsService extends Service {
-     /**
-      * The Intent action that a CustomTabsService must respond to.
-      */
-     public static final String ACTION_CUSTOM_TABS_CONNECTION =
-             "android.support.customtabs.action.CustomTabsService";
+public abstract class CustomTabsService extends Service {
+    /**
+     * The Intent action that a CustomTabsService must respond to.
+     */
+    public static final String ACTION_CUSTOM_TABS_CONNECTION =
+            "android.support.customtabs.action.CustomTabsService";
 
-     /**
-      * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
-      * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
-      * to insert a new url to each bundle inside list of bundles.
-      */
-     public static final String KEY_URL =
-             "android.support.customtabs.otherurls.URL";
+    /**
+     * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
+     * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
+     * to insert a new url to each bundle inside list of bundles.
+     */
+    public static final String KEY_URL =
+            "android.support.customtabs.otherurls.URL";
 
-     private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({RESULT_SUCCESS, RESULT_FAILURE_DISALLOWED,
+            RESULT_FAILURE_REMOTE_ERROR, RESULT_FAILURE_MESSAGING_ERROR})
+    public @interface Result {
+    }
 
-     private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
+    /**
+     * Indicates that the postMessage request was accepted.
+     */
+    public static final int RESULT_SUCCESS = 0;
+    /**
+     * Indicates that the postMessage request was not allowed due to a bad argument or requesting
+     * at a disallowed time like when in background.
+     */
+    public static final int RESULT_FAILURE_DISALLOWED = -1;
+    /**
+     * Indicates that the postMessage request has failed due to a {@link RemoteException} .
+     */
+    public static final int RESULT_FAILURE_REMOTE_ERROR = -2;
+    /**
+     * Indicates that the postMessage request has failed due to an internal error on the browser
+     * message channel.
+     */
+    public static final int RESULT_FAILURE_MESSAGING_ERROR = -3;
 
-         @Override
-         public boolean warmup(long flags) {
-             return CustomTabsService.this.warmup(flags);
-         }
+    private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
 
-         @Override
-         public boolean newSession(ICustomTabsCallback callback) {
-             final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
-             try {
-                 DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
-                     @Override
-                     public void binderDied() {
-                         cleanUpSession(sessionToken);
-                     }
-                 };
-                 synchronized (mDeathRecipientMap) {
-                     callback.asBinder().linkToDeath(deathRecipient, 0);
-                     mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
-                 }
-                 return CustomTabsService.this.newSession(sessionToken);
-             } catch (RemoteException e) {
-                 return false;
-             }
-         }
+    private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
 
-         @Override
-         public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
-                         Bundle extras, List<Bundle> otherLikelyBundles) {
-             return CustomTabsService.this.mayLaunchUrl(
-                     new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
-         }
+        @Override
+        public boolean warmup(long flags) {
+            return CustomTabsService.this.warmup(flags);
+        }
 
-         @Override
-         public Bundle extraCommand(String commandName, Bundle args) {
-             return CustomTabsService.this.extraCommand(commandName, args);
-         }
+        @Override
+        public boolean newSession(ICustomTabsCallback callback) {
+            final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
+            try {
+                DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
+                    @Override
+                    public void binderDied() {
+                        cleanUpSession(sessionToken);
+                    }
+                };
+                synchronized (mDeathRecipientMap) {
+                    callback.asBinder().linkToDeath(deathRecipient, 0);
+                    mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
+                }
+                return CustomTabsService.this.newSession(sessionToken);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
 
-         @Override
-         public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
-             return CustomTabsService.this.updateVisuals(
-                     new CustomTabsSessionToken(callback), bundle);
-         }
-     };
+        @Override
+        public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
+                                    Bundle extras, List<Bundle> otherLikelyBundles) {
+            return CustomTabsService.this.mayLaunchUrl(
+                    new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
+        }
 
-     @Override
-     public IBinder onBind(Intent intent) {
-         return mBinder;
-     }
+        @Override
+        public Bundle extraCommand(String commandName, Bundle args) {
+            return CustomTabsService.this.extraCommand(commandName, args);
+        }
 
-     /**
-      * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
-      * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
-      * @param sessionToken The session token for which the {@link DeathRecipient} call has been
-      *                     received.
-      * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
-      *         same binder will return false.
-      */
-     protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
-         try {
-             synchronized (mDeathRecipientMap) {
+        @Override
+        public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
+            return CustomTabsService.this.updateVisuals(
+                    new CustomTabsSessionToken(callback), bundle);
+        }
+
+        @Override
+        public boolean requestPostMessageChannel(ICustomTabsCallback callback,
+                                                 Uri postMessageOrigin) {
+            return CustomTabsService.this.requestPostMessageChannel(
+                    new CustomTabsSessionToken(callback), postMessageOrigin);
+        }
+
+        @Override
+        public int postMessage(ICustomTabsCallback callback, String message, Bundle extras) {
+            return CustomTabsService.this.postMessage(
+                    new CustomTabsSessionToken(callback), message, extras);
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
+     * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
+     *
+     * @param sessionToken The session token for which the {@link DeathRecipient} call has been
+     *                     received.
+     * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
+     * same binder will return false.
+     */
+    protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
+        try {
+            synchronized (mDeathRecipientMap) {
                 IBinder binder = sessionToken.getCallbackBinder();
                 DeathRecipient deathRecipient =
                         mDeathRecipientMap.get(binder);
                 binder.unlinkToDeath(deathRecipient, 0);
                 mDeathRecipientMap.remove(binder);
             }
-         } catch (NoSuchElementException e) {
-             return false;
-         }
-         return true;
-     }
+        } catch (NoSuchElementException e) {
+            return false;
+        }
+        return true;
+    }
 
-     /**
-      * Warms up the browser process asynchronously.
-      *
-      * @param flags Reserved for future use.
-      * @return      Whether warmup was/had been completed successfully. Multiple successful
-      *              calls will return true.
-      */
-     protected abstract boolean warmup(long flags);
+    /**
+     * Warms up the browser process asynchronously.
+     *
+     * @param flags Reserved for future use.
+     * @return Whether warmup was/had been completed successfully. Multiple successful
+     * calls will return true.
+     */
+    protected abstract boolean warmup(long flags);
 
-     /**
-      * Creates a new session through an ICustomTabsService with the optional callback. This session
-      * can be used to associate any related communication through the service with an intent and
-      * then later with a Custom Tab. The client can then send later service calls or intents to
-      * through same session-intent-Custom Tab association.
-      * @param sessionToken Session token to be used as a unique identifier. This also has access
-      *                     to the {@link CustomTabsCallback} passed from the client side through
-      *                     {@link CustomTabsSessionToken#getCallback()}.
-      * @return             Whether a new session was successfully created.
-      */
-     protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
+    /**
+     * Creates a new session through an ICustomTabsService with the optional callback. This session
+     * can be used to associate any related communication through the service with an intent and
+     * then later with a Custom Tab. The client can then send later service calls or intents to
+     * through same session-intent-Custom Tab association.
+     *
+     * @param sessionToken Session token to be used as a unique identifier. This also has access
+     *                     to the {@link CustomTabsCallback} passed from the client side through
+     *                     {@link CustomTabsSessionToken#getCallback()}.
+     * @return Whether a new session was successfully created.
+     */
+    protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
 
-     /**
-      * Tells the browser of a likely future navigation to a URL.
-      *
-      * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
-      * The most likely URL has to be specified explicitly. Optionally, a list of
-      * other likely URLs can be provided. They are treated as less likely than
-      * the first one, and have to be sorted in decreasing priority order. These
-      * additional URLs may be ignored.
-      * All previous calls to this method will be deprioritized.
-      *
-      * @param sessionToken       The unique identifier for the session. Can not be null.
-      * @param url                Most likely URL.
-      * @param extras             Reserved for future use.
-      * @param otherLikelyBundles Other likely destinations, sorted in decreasing
-      *                           likelihood order. Each Bundle has to provide a url.
-      * @return                   Whether the call was successful.
-      */
-     protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
-             Bundle extras, List<Bundle> otherLikelyBundles);
+    /**
+     * Tells the browser of a likely future navigation to a URL.
+     * <p>
+     * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
+     * The most likely URL has to be specified explicitly. Optionally, a list of
+     * other likely URLs can be provided. They are treated as less likely than
+     * the first one, and have to be sorted in decreasing priority order. These
+     * additional URLs may be ignored.
+     * All previous calls to this method will be deprioritized.
+     *
+     * @param sessionToken       The unique identifier for the session. Can not be null.
+     * @param url                Most likely URL.
+     * @param extras             Reserved for future use.
+     * @param otherLikelyBundles Other likely destinations, sorted in decreasing
+     *                           likelihood order. Each Bundle has to provide a url.
+     * @return Whether the call was successful.
+     */
+    protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
+                                            Bundle extras, List<Bundle> otherLikelyBundles);
 
-     /**
-      * Unsupported commands that may be provided by the implementation.
-      *
-      * <p>
-      * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
-      * defined behavior, as it is entirely implementation-defined and not supported.
-      *
-      * <p> This call can be used by implementations to add extra commands, for testing or
-      * experimental purposes.
-      *
-      * @param commandName Name of the extra command to execute.
-      * @param args Arguments for the command
-      * @return The result {@link Bundle}, or null.
-      */
-     protected abstract Bundle extraCommand(String commandName, Bundle args);
+    /**
+     * Unsupported commands that may be provided by the implementation.
+     * <p>
+     * <p>
+     * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
+     * defined behavior, as it is entirely implementation-defined and not supported.
+     * <p>
+     * <p> This call can be used by implementations to add extra commands, for testing or
+     * experimental purposes.
+     *
+     * @param commandName Name of the extra command to execute.
+     * @param args        Arguments for the command
+     * @return The result {@link Bundle}, or null.
+     */
+    protected abstract Bundle extraCommand(String commandName, Bundle args);
 
     /**
      * Updates the visuals of custom tabs for the given session. Will only succeed if the given
      * session matches the currently active one.
+     *
      * @param sessionToken The currently active session that the custom tab belongs to.
      * @param bundle       The action button configuration bundle. This bundle should be constructed
      *                     with the same structure in {@link CustomTabsIntent.Builder}.
      * @return Whether the operation was successful.
      */
-     protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
-             Bundle bundle);
- }
+    protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
+                                             Bundle bundle);
+
+    /**
+     * Sends a request to create a two way postMessage channel between the client and the browser
+     * linked with the given {@link CustomTabsSession}.
+     *
+     * @param sessionToken      The unique identifier for the session. Can not be null.
+     * @param postMessageOrigin A origin that the client is requesting to be identified as
+     *                          during the postMessage communication.
+     * @return Whether the implementation accepted the request. Note that returning true
+     * here doesn't mean an origin has already been assigned as the validation is
+     * asynchronous.
+     */
+    protected abstract boolean requestPostMessageChannel(CustomTabsSessionToken sessionToken,
+                                                         Uri postMessageOrigin);
+
+    /**
+     * Sends a postMessage request using the origin communicated via
+     * {@link CustomTabsService#requestPostMessageChannel(
+     *CustomTabsSessionToken, Uri)}. Fails when called before
+     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on the
+     * client side.
+     *
+     * @param sessionToken The unique identifier for the session. Can not be null.
+     * @param message      The message that is being sent.
+     * @param extras       Reserved for future use.
+     * @return An integer constant about the postMessage request result. Will return
+     * {@link CustomTabsService#RESULT_SUCCESS} if successful.
+     */
+    @Result
+    protected abstract int postMessage(
+            CustomTabsSessionToken sessionToken, String message, Bundle extras);
+}
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSession.java b/customtabs/src/android/support/customtabs/CustomTabsSession.java
index 1136177..cad897c 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSession.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSession.java
@@ -25,6 +25,7 @@
 import android.os.RemoteException;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.customtabs.CustomTabsService.Result;
 import android.view.View;
 import android.widget.RemoteViews;
 
@@ -36,6 +37,7 @@
  */
 public final class CustomTabsSession {
     private static final String TAG = "CustomTabsSession";
+    private final Object mLock = new Object();
     private final ICustomTabsService mService;
     private final ICustomTabsCallback mCallback;
     private final ComponentName mComponentName;
@@ -142,6 +144,47 @@
         }
     }
 
+    /**
+     * Sends a request to create a two way postMessage channel between the client and the browser.
+     *
+     * @param postMessageOrigin      A origin that the client is requesting to be identified as
+     *                               during the postMessage communication.
+     * @return Whether the implementation accepted the request. Note that returning true
+     *         here doesn't mean an origin has already been assigned as the validation is
+     *         asynchronous.
+     */
+    public boolean requestPostMessageChannel(Uri postMessageOrigin) {
+        try {
+            return mService.requestPostMessageChannel(
+                    mCallback, postMessageOrigin);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Sends a postMessage request using the origin communicated via
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, Uri)}. Fails when called before
+     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on
+     * the client side.
+     *
+     * @param message The message that is being sent.
+     * @param extras Reserved for future use.
+     * @return An integer constant about the postMessage request result. Will return
+      *        {@link CustomTabsService#RESULT_SUCCESS} if successful.
+     */
+    @Result
+    public int postMessage(String message, Bundle extras) {
+        synchronized (mLock) {
+            try {
+                return mService.postMessage(mCallback, message, extras);
+            } catch (RemoteException e) {
+                return CustomTabsService.RESULT_FAILURE_REMOTE_ERROR;
+            }
+        }
+    }
+
     /* package */ IBinder getBinder() {
         return mCallback.asBinder();
     }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
index fdb5f91..adfadd9 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
@@ -58,6 +58,33 @@
                     Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
                 }
             }
+
+            @Override
+            public void extraCallback(String callbackName, Bundle args) {
+                try {
+                    mCallbackBinder.extraCallback(callbackName, args);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
+
+            @Override
+            public void onMessageChannelReady(Bundle extras) {
+                try {
+                    mCallbackBinder.onMessageChannelReady(extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
+
+            @Override
+            public void onPostMessage(String message, Bundle extras) {
+                try {
+                    mCallbackBinder.onPostMessage(message, extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+                }
+            }
         };
     }
 
@@ -84,4 +111,11 @@
     public CustomTabsCallback getCallback() {
         return mCallback;
     }
-}
\ No newline at end of file
+
+    /**
+     * @return Whether this token is associated with the given session.
+     */
+    public boolean isAssociatedWith(CustomTabsSession session) {
+        return session.getBinder().equals(mCallbackBinder);
+    }
+}
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
index a467864..32b6e9b 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
@@ -22,7 +22,9 @@
  * Interface to a CustomTabsCallback.
  * @hide
  */
-oneway interface ICustomTabsCallback {
+interface ICustomTabsCallback {
     void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
     void extraCallback(String callbackName, in Bundle args) = 2;
+    void onMessageChannelReady(in Bundle extras) = 3;
+    void onPostMessage(String message, in Bundle extras) = 4;
 }
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
index e53ca84..b24b0dd 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
@@ -16,6 +16,7 @@
 
 package android.support.customtabs;
 
+import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.customtabs.ICustomTabsCallback;
@@ -33,4 +34,6 @@
             in Bundle extras, in List<Bundle> otherLikelyBundles) = 3;
     Bundle extraCommand(String commandName, in Bundle args) = 4;
     boolean updateVisuals(in ICustomTabsCallback callback, in Bundle bundle) = 5;
+    boolean requestPostMessageChannel(in ICustomTabsCallback callback, in Uri postMessageOrigin) = 6;
+    int postMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 7;
 }
diff --git a/customtabs/src/android/support/customtabs/IPostMessageService.aidl b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
new file mode 100644
index 0000000..2c8a605
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.customtabs.ICustomTabsCallback;
+
+/**
+ * Interface to a PostMessageService.
+ * @hide
+ */
+interface IPostMessageService {
+    void onMessageChannelReady(in ICustomTabsCallback callback, in Bundle extras) = 1;
+    void onPostMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 2;
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageService.java b/customtabs/src/android/support/customtabs/PostMessageService.java
new file mode 100644
index 0000000..7355f4e
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageService.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A service to receive postMessage related communication from a Custom Tabs provider.
+ */
+public class PostMessageService extends Service {
+    private IPostMessageService.Stub mBinder = new IPostMessageService.Stub() {
+
+        @Override
+        public void onMessageChannelReady(
+                ICustomTabsCallback callback, Bundle extras) throws RemoteException {
+            callback.onMessageChannelReady(extras);
+        }
+
+        @Override
+        public void onPostMessage(ICustomTabsCallback callback,
+                                  String message, Bundle extras) throws RemoteException {
+            callback.onPostMessage(message, extras);
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
new file mode 100644
index 0000000..4eef50c
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A {@link ServiceConnection} for Custom Tabs providers to use while connecting to a
+ * {@link PostMessageService} on the client side.
+ */
+public abstract class PostMessageServiceConnection implements ServiceConnection {
+    private final Object mLock = new Object();
+    private final ICustomTabsCallback mSessionBinder;
+    private IPostMessageService mService;
+
+    public PostMessageServiceConnection(CustomTabsSessionToken session) {
+        mSessionBinder = ICustomTabsCallback.Stub.asInterface(session.getCallbackBinder());
+    }
+
+    /**
+     * Binds the browser side to the client app through the given {@link PostMessageService} name.
+     * After this, this {@link PostMessageServiceConnection} can be used for sending postMessage
+     * related communication back to the client.
+     * @param context A context to bind to the service.
+     * @param packageName The name of the package to be bound to.
+     * @return Whether the binding was successful.
+     */
+    public boolean bindSessionToPostMessageService(Context context, String packageName) {
+        Intent intent = new Intent();
+        intent.setClassName(packageName, PostMessageService.class.getName());
+        return context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+    }
+
+    /**
+     * Unbinds this service connection from the given context.
+     * @param context The context to be unbound from.
+     */
+    public void unbindFromContext(Context context) {
+        context.unbindService(this);
+    }
+
+    @Override
+    public final void onServiceConnected(ComponentName name, IBinder service) {
+        mService = IPostMessageService.Stub.asInterface(service);
+        onPostMessageServiceConnected();
+    }
+
+    @Override
+    public final void onServiceDisconnected(ComponentName name) {
+        mService = null;
+        onPostMessageServiceDisconnected();
+    }
+
+    /**
+     * Notifies the client that the postMessage channel requested with
+     * {@link CustomTabsService#requestPostMessageChannel(
+     * CustomTabsSessionToken, android.net.Uri)} is ready. This method should be
+     * called when the browser binds to the client side {@link PostMessageService} and also readies
+     * a connection to the web frame.
+     *
+     * @param extras Reserved for future use.
+     * @return Whether the notification was sent to the remote successfully.
+     */
+    public final boolean notifyMessageChannelReady(Bundle extras) {
+        if (mService == null) return false;
+        synchronized (mLock) {
+            try {
+                mService.onMessageChannelReady(mSessionBinder, extras);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Posts a message to the client. This should be called when a tab controlled by related
+     * {@link CustomTabsSession} has sent a postMessage. If postMessage() is called from a single
+     * thread, then the messages will be posted in the same order.
+     *
+     * @param message The message sent.
+     * @param extras Reserved for future use.
+     * @return Whether the postMessage was sent to the remote successfully.
+     */
+    public final boolean postMessage(String message, Bundle extras) {
+        if (mService == null) return false;
+        synchronized (mLock) {
+            try {
+                mService.onPostMessage(mSessionBinder, message, extras);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Called when the {@link PostMessageService} connection is established.
+     */
+    public void onPostMessageServiceConnected() {}
+
+    /**
+     * Called when the connection is lost with the {@link PostMessageService}.
+     */
+    public void onPostMessageServiceDisconnected() {}
+}
diff --git a/customtabs/tests/src/AndroidManifest.xml b/customtabs/tests/AndroidManifest.xml
similarity index 76%
rename from customtabs/tests/src/AndroidManifest.xml
rename to customtabs/tests/AndroidManifest.xml
index d87a83f..6fe8ad9 100644
--- a/customtabs/tests/src/AndroidManifest.xml
+++ b/customtabs/tests/AndroidManifest.xml
@@ -23,9 +23,12 @@
         tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
               android.support.test.espresso, android.support.test.espresso.idling" />
 
-    <application/>
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity   android:name="android.support.customtabs.TestActivity"/>
 
-    <instrumentation
-            android:name="android.test.InstrumentationTestRunner"
-            android:targetPackage="android.support.customtabs.test"/>
+        <service    android:name="android.support.customtabs.PostMessageService"/>
+
+        <service    android:name="android.support.customtabs.TestCustomTabsService"/>
+    </application>
 </manifest>
diff --git a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
index 0fc69f9..c965361 100644
--- a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
+++ b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
@@ -39,6 +39,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CustomTabsIntentTest {
+
     @Test
     public void testBareboneCustomTabIntent() {
         CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
diff --git a/customtabs/tests/src/android/support/customtabs/PollingCheck.java b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
new file mode 100644
index 0000000..0163e94
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+public abstract class PollingCheck {
+    private static final long TIME_SLICE = 50;
+    private long mTimeout = 3000;
+
+    public interface PollingCheckCondition {
+        boolean canProceed();
+    }
+
+    public PollingCheck() {
+    }
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        while (timeout > 0) {
+            if (condition.call()) {
+                return;
+            }
+
+            Thread.sleep(TIME_SLICE);
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail(message.toString());
+    }
+
+    public static void waitFor(final PollingCheckCondition condition) {
+        new PollingCheck() {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+
+    public static void waitFor(long timeout, final PollingCheckCondition condition) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
new file mode 100644
index 0000000..a2b1b31
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests for {@link PostMessageServiceConnection} with no {@link CustomTabsService} component.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageServiceConnectionTest {
+    @Rule
+    public final ServiceTestRule mServiceRule;
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule;
+    private TestCustomTabsCallback mCallback;
+    private Context mContext;
+    private PostMessageServiceConnection mConnection;
+    private boolean mServiceConnected;
+
+
+    public PostMessageServiceConnectionTest() {
+        mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+        mServiceRule = new ServiceTestRule();
+    }
+
+    @Before
+    public void setup() {
+        mCallback = new TestCustomTabsCallback();
+        mContext = mActivityTestRule.getActivity();
+        mConnection = new PostMessageServiceConnection(
+                new CustomTabsSessionToken(mCallback.getStub())) {
+            public void onPostMessageServiceConnected() {
+                mServiceConnected = true;
+            }
+
+            @Override
+            public void onPostMessageServiceDisconnected() {
+                mServiceConnected = false;
+            }
+        };
+        Intent intent = new Intent();
+        intent.setClassName(mContext.getPackageName(), PostMessageService.class.getName());
+        try {
+            mServiceRule.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        } catch (TimeoutException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void testNotifyChannelCreationAndSendMessages() {
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mServiceConnected;
+            }
+        });
+        assertTrue(mServiceConnected);
+        mConnection.notifyMessageChannelReady(null);
+        assertTrue(mCallback.isMessageChannelReady());
+        mConnection.postMessage("message1", null);
+        assertEquals(mCallback.getMessages().size(), 1);
+        mConnection.postMessage("message2", null);
+        assertEquals(mCallback.getMessages().size(), 2);
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
new file mode 100644
index 0000000..d20a06d
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+/**
+ * Tests for a complete loop between a browser side {@link CustomTabsService}
+ * and a client side {@link PostMessageService}. Both services are bound to through
+ * {@link ServiceTestRule}, but {@link CustomTabsCallback#extraCallback} is used to link browser
+ * side actions.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageTest {
+    @Rule
+    public final ServiceTestRule mServiceRule;
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule;
+    private TestCustomTabsCallback mCallback;
+    private Context mContext;
+    private CustomTabsServiceConnection mCustomTabsServiceConnection;
+    private PostMessageServiceConnection mPostMessageServiceConnection;
+    private AtomicBoolean mCustomTabsServiceConnected;
+    private boolean mPostMessageServiceConnected;
+    private CustomTabsSession mSession;
+
+    public PostMessageTest() {
+        mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+        mServiceRule = new ServiceTestRule();
+        mCustomTabsServiceConnected = new AtomicBoolean(false);
+    }
+
+
+    @Before
+    public void setup() {
+        // Bind to PostMessageService only after CustomTabsService sends the callback to do so. This
+        // callback is sent after requestPostMessageChannel is called.
+        mCallback = new TestCustomTabsCallback() {
+            @Override
+            public void extraCallback(String callbackName, Bundle args) {
+                if (TestCustomTabsService.CALLBACK_BIND_TO_POST_MESSAGE.equals(callbackName)) {
+                    Intent postMessageServiceIntent = new Intent();
+                    postMessageServiceIntent.setClassName(
+                            mContext.getPackageName(), PostMessageService.class.getName());
+                    try {
+                        mServiceRule.bindService(postMessageServiceIntent,
+                                mPostMessageServiceConnection, Context.BIND_AUTO_CREATE);
+                    } catch (TimeoutException e) {
+                        fail();
+                    }
+                }
+            }
+        };
+        mContext = mActivityTestRule.getActivity();
+        mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
+            @Override
+            public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
+                mSession = client.newSession(mCallback);
+                mCustomTabsServiceConnected.set(true);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName componentName) {
+                mCustomTabsServiceConnected.set(false);
+            }
+        };
+        mPostMessageServiceConnection = new PostMessageServiceConnection(
+                new CustomTabsSessionToken(mCallback.getStub())) {
+            @Override
+            public void onPostMessageServiceConnected() {
+                mPostMessageServiceConnected = true;
+            }
+
+            @Override
+            public void onPostMessageServiceDisconnected() {
+                mPostMessageServiceConnected = false;
+            }
+        };
+        Intent customTabsServiceIntent = new Intent();
+        customTabsServiceIntent.setClassName(
+                mContext.getPackageName(), TestCustomTabsService.class.getName());
+        try {
+            mServiceRule.bindService(customTabsServiceIntent,
+                    mCustomTabsServiceConnection, Context.BIND_AUTO_CREATE);
+        } catch (TimeoutException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void testCustomTabsConnection() {
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mCustomTabsServiceConnected.get();
+            }
+        });
+        assertTrue(mCustomTabsServiceConnected.get());
+        assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
+        assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
+        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mPostMessageServiceConnected;
+            }
+        });
+        assertTrue(mPostMessageServiceConnected);
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestActivity.java b/customtabs/tests/src/android/support/customtabs/TestActivity.java
new file mode 100644
index 0000000..1d7941a
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+/**
+ * Simple test activity for custom tabs testing.
+ */
+
+public class TestActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        setContentView(new FrameLayout(this));
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
new file mode 100644
index 0000000..56b1817
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * A test class to check the incoming messages through {@link CustomTabsCallback}.
+ */
+public class TestCustomTabsCallback extends CustomTabsCallback {
+    private boolean mOnMessageChannelReady;
+    private ArrayList<String> mMessageList = new ArrayList<>();
+    private ICustomTabsCallback.Stub mWrapper = new ICustomTabsCallback.Stub() {
+        @Override
+        public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+            TestCustomTabsCallback.this.onNavigationEvent(navigationEvent, extras);
+        }
+
+        @Override
+        public void extraCallback(final String callbackName, final Bundle args)
+                throws RemoteException {
+            TestCustomTabsCallback.this.extraCallback(callbackName, args);
+        }
+
+        @Override
+        public void onMessageChannelReady(final Bundle extras)
+                throws RemoteException {
+            TestCustomTabsCallback.this.onMessageChannelReady(extras);
+        }
+
+        @Override
+        public void onPostMessage(final String message, final Bundle extras)
+                throws RemoteException {
+            TestCustomTabsCallback.this.onPostMessage(message, extras);
+        }
+    };
+
+    /* package */ ICustomTabsCallback getStub() {
+        return mWrapper;
+    }
+
+    @Override
+    public void onMessageChannelReady(Bundle extras) {
+        mOnMessageChannelReady = true;
+    }
+
+    /**
+     * @return Whether the message channel is ready.
+     */
+    public boolean isMessageChannelReady() {
+        return mOnMessageChannelReady;
+    }
+
+    @Override
+    public void onPostMessage(String message, Bundle extras) {
+        mMessageList.add(message);
+    }
+
+    /**
+     * @return A list of messages that have been sent so far.
+     */
+    public ArrayList<String> getMessages() {
+        return mMessageList;
+    }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
new file mode 100644
index 0000000..b5c5e86
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * A test class that simulates how a {@link CustomTabsService} would behave.
+ */
+
+public class TestCustomTabsService extends CustomTabsService {
+    public static final String CALLBACK_BIND_TO_POST_MESSAGE = "BindToPostMessageService";
+    private boolean mPostMessageRequested;
+    private CustomTabsSessionToken mSession;
+
+    @Override
+    protected boolean warmup(long flags) {
+        return false;
+    }
+
+    @Override
+    protected boolean newSession(CustomTabsSessionToken sessionToken) {
+        mSession = sessionToken;
+        return true;
+    }
+
+    @Override
+    protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken,
+                                   Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
+        return false;
+    }
+
+    @Override
+    protected Bundle extraCommand(String commandName, Bundle args) {
+        return null;
+    }
+
+    @Override
+    protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) {
+        return false;
+    }
+
+    @Override
+    protected boolean requestPostMessageChannel(
+            CustomTabsSessionToken sessionToken, Uri postMessageOrigin) {
+        if (mSession == null) return false;
+        mPostMessageRequested = true;
+        mSession.getCallback().extraCallback(CALLBACK_BIND_TO_POST_MESSAGE, null);
+        return true;
+    }
+
+    @Override
+    protected int postMessage(CustomTabsSessionToken sessionToken, String message, Bundle extras) {
+        if (!mPostMessageRequested) return CustomTabsService.RESULT_FAILURE_DISALLOWED;
+        return CustomTabsService.RESULT_SUCCESS;
+    }
+}
diff --git a/design/Android.mk b/design/Android.mk
index b3bc846..2e634eb 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -38,6 +38,7 @@
     $(call all-java-files-under,lollipop) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-transition \
     android-support-v7-appcompat \
diff --git a/design/AndroidManifest-make.xml b/design/AndroidManifest-make.xml
new file mode 100644
index 0000000..d51186dde
--- /dev/null
+++ b/design/AndroidManifest-make.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.design">
+    <uses-sdk android:minSdkVersion="9"
+              tools:overrideLibrary="android.support.transition"/>
+    <application />
+</manifest>
diff --git a/design/AndroidManifest.xml b/design/AndroidManifest.xml
index d51186dde..2d5fe0b 100644
--- a/design/AndroidManifest.xml
+++ b/design/AndroidManifest.xml
@@ -18,5 +18,6 @@
           package="android.support.design">
     <uses-sdk android:minSdkVersion="9"
               tools:overrideLibrary="android.support.transition"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/design/build.gradle b/design/build.gradle
index 55808bb..d5f83b8 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -89,28 +89,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/design/src/android/support/design/internal/BottomNavigationItemView.java b/design/src/android/support/design/internal/BottomNavigationItemView.java
index 44db0ea..ad05e76 100644
--- a/design/src/android/support/design/internal/BottomNavigationItemView.java
+++ b/design/src/android/support/design/internal/BottomNavigationItemView.java
@@ -122,6 +122,7 @@
     public void setTitle(CharSequence title) {
         mSmallLabel.setText(title);
         mLargeLabel.setText(title);
+        setContentDescription(title);
     }
 
     @Override
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index 82d983e..5d7c19d 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -31,6 +31,7 @@
 import android.support.v7.view.menu.MenuItemImpl;
 import android.support.v7.view.menu.MenuView;
 import android.util.AttributeSet;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -45,13 +46,13 @@
     private final int mItemHeight;
     private final OnClickListener mOnClickListener;
     private final BottomNavigationAnimationHelperBase mAnimationHelper;
-    private static final Pools.Pool<BottomNavigationItemView> sItemPool =
-            new Pools.SynchronizedPool<>(5);
+    private final Pools.Pool<BottomNavigationItemView> mItemPool = new Pools.SynchronizedPool<>(5);
 
     private boolean mShiftingMode = true;
 
     private BottomNavigationItemView[] mButtons;
-    private int mActiveButton = 0;
+    private int mSelectedItemId = 0;
+    private int mSelectedItemPosition = 0;
     private ColorStateList mItemIconTint;
     private ColorStateList mItemTextColor;
     private int mItemBackgroundRes;
@@ -85,9 +86,9 @@
             @Override
             public void onClick(View v) {
                 final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
-                final int itemPosition = itemView.getItemPosition();
-                if (!mMenu.performItemAction(itemView.getItemData(), mPresenter, 0)) {
-                    activateNewButton(itemPosition);
+                MenuItem item = itemView.getItemData();
+                if (!mMenu.performItemAction(item, mPresenter, 0)) {
+                    item.setChecked(true);
                 }
             }
         };
@@ -114,7 +115,7 @@
             final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
             int extra = width - activeWidth - inactiveWidth * inactiveCount;
             for (int i = 0; i < count; i++) {
-                mTempChildWidths[i] = (i == mActiveButton) ? activeWidth : inactiveWidth;
+                mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
                 if (extra > 0) {
                     mTempChildWidths[i]++;
                     extra--;
@@ -248,13 +249,15 @@
     }
 
     public void buildMenuView() {
+        removeAllViews();
         if (mButtons != null) {
             for (BottomNavigationItemView item : mButtons) {
-                sItemPool.release(item);
+                mItemPool.release(item);
             }
         }
-        removeAllViews();
         if (mMenu.size() == 0) {
+            mSelectedItemId = 0;
+            mSelectedItemPosition = 0;
             mButtons = null;
             return;
         }
@@ -275,8 +278,8 @@
             child.setOnClickListener(mOnClickListener);
             addView(child);
         }
-        mActiveButton = Math.min(mMenu.size() - 1, mActiveButton);
-        mMenu.getItem(mActiveButton).setChecked(true);
+        mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
+        mMenu.getItem(mSelectedItemPosition).setChecked(true);
     }
 
     public void updateMenuView() {
@@ -286,31 +289,44 @@
             buildMenuView();
             return;
         }
+        int previousSelectedId = mSelectedItemId;
         for (int i = 0; i < menuSize; i++) {
             mPresenter.setUpdateSuspended(true);
-            if (mMenu.getItem(i).isChecked()) {
-                mActiveButton = i;
+            MenuItem item = mMenu.getItem(i);
+            if (item.isChecked()) {
+                mSelectedItemId = item.getItemId();
+                mSelectedItemPosition = i;
             }
-            mButtons[i].initialize((MenuItemImpl) mMenu.getItem(i), 0);
+            mButtons[i].initialize((MenuItemImpl) item, 0);
             mPresenter.setUpdateSuspended(false);
         }
+        if (previousSelectedId != mSelectedItemId) {
+            mAnimationHelper.beginDelayedTransition(this);
+        }
     }
 
-    private void activateNewButton(int newButton) {
-        if (mActiveButton == newButton) return;
-
-        mAnimationHelper.beginDelayedTransition(this);
-
-        mMenu.getItem(newButton).setChecked(true);
-
-        mActiveButton = newButton;
-    }
-
     private BottomNavigationItemView getNewItem() {
-        BottomNavigationItemView item = sItemPool.acquire();
+        BottomNavigationItemView item = mItemPool.acquire();
         if (item == null) {
             item = new BottomNavigationItemView(getContext());
         }
         return item;
     }
+
+    public int getSelectedItemId() {
+        return mSelectedItemId;
+    }
+
+    void tryRestoreSelectedItemId(int itemId) {
+        final int size = mMenu.size();
+        for (int i = 0; i < size; i++) {
+            MenuItem item = mMenu.getItem(i);
+            if (itemId == item.getItemId()) {
+                mSelectedItemId = itemId;
+                mSelectedItemPosition = i;
+                item.setChecked(true);
+                break;
+            }
+        }
+    }
 }
diff --git a/design/src/android/support/design/internal/BottomNavigationPresenter.java b/design/src/android/support/design/internal/BottomNavigationPresenter.java
index 832e936..1343a4b 100644
--- a/design/src/android/support/design/internal/BottomNavigationPresenter.java
+++ b/design/src/android/support/design/internal/BottomNavigationPresenter.java
@@ -19,7 +19,9 @@
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.content.Context;
+import android.os.Parcel;
 import android.os.Parcelable;
+import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.v7.view.menu.MenuBuilder;
 import android.support.v7.view.menu.MenuItemImpl;
@@ -36,6 +38,7 @@
     private MenuBuilder mMenu;
     private BottomNavigationMenuView mMenuView;
     private boolean mUpdateSuspended = false;
+    private int mId;
 
     public void setBottomNavigationMenuView(BottomNavigationMenuView menuView) {
         mMenuView = menuView;
@@ -88,20 +91,62 @@
         return false;
     }
 
+    public void setId(int id) {
+        mId = id;
+    }
+
     @Override
     public int getId() {
-        return -1;
+        return mId;
     }
 
     @Override
     public Parcelable onSaveInstanceState() {
-        return null;
+        SavedState savedState = new SavedState();
+        savedState.selectedItemId = mMenuView.getSelectedItemId();
+        return savedState;
     }
 
     @Override
-    public void onRestoreInstanceState(Parcelable state) {}
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            mMenuView.tryRestoreSelectedItemId(((SavedState) state).selectedItemId);
+        }
+    }
 
     public void setUpdateSuspended(boolean updateSuspended) {
         mUpdateSuspended = updateSuspended;
     }
+
+    static class SavedState implements Parcelable {
+        int selectedItemId;
+
+        SavedState() {}
+
+        SavedState(Parcel in) {
+            selectedItemId = in.readInt();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel out, int flags) {
+            out.writeInt(selectedItemId);
+        }
+
+        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
 }
diff --git a/design/src/android/support/design/internal/NavigationMenuPresenter.java b/design/src/android/support/design/internal/NavigationMenuPresenter.java
index a17189c..98ad468 100644
--- a/design/src/android/support/design/internal/NavigationMenuPresenter.java
+++ b/design/src/android/support/design/internal/NavigationMenuPresenter.java
@@ -560,7 +560,8 @@
             }
             // Store the states of the action views.
             SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>();
-            for (NavigationMenuItem navigationMenuItem : mItems) {
+            for (int i = 0, size = mItems.size(); i < size; i++) {
+                NavigationMenuItem navigationMenuItem = mItems.get(i);
                 if (navigationMenuItem instanceof NavigationMenuTextItem) {
                     MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
                     View actionView = item != null ? item.getActionView() : null;
@@ -579,7 +580,8 @@
             int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0);
             if (checkedItem != 0) {
                 mUpdateSuspended = true;
-                for (NavigationMenuItem item : mItems) {
+                for (int i = 0, size = mItems.size(); i < size; i++) {
+                    NavigationMenuItem item = mItems.get(i);
                     if (item instanceof NavigationMenuTextItem) {
                         MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem();
                         if (menuItem != null && menuItem.getItemId() == checkedItem) {
@@ -594,13 +596,25 @@
             // Restore the states of the action views.
             SparseArray<ParcelableSparseArray> actionViewStates = state
                     .getSparseParcelableArray(STATE_ACTION_VIEWS);
-            for (NavigationMenuItem navigationMenuItem : mItems) {
-                if (navigationMenuItem instanceof NavigationMenuTextItem) {
-                    MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
-                    View actionView = item != null ? item.getActionView() : null;
-                    if (actionView != null) {
-                        actionView.restoreHierarchyState(actionViewStates.get(item.getItemId()));
+            if (actionViewStates != null) {
+                for (int i = 0, size = mItems.size(); i < size; i++) {
+                    NavigationMenuItem navigationMenuItem = mItems.get(i);
+                    if (!(navigationMenuItem instanceof NavigationMenuTextItem)) {
+                        continue;
                     }
+                    MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
+                    if (item == null) {
+                        continue;
+                    }
+                    View actionView = item.getActionView();
+                    if (actionView == null) {
+                        continue;
+                    }
+                    ParcelableSparseArray container = actionViewStates.get(item.getItemId());
+                    if (container == null) {
+                        continue;
+                    }
+                    actionView.restoreHierarchyState(container);
                 }
             }
         }
diff --git a/design/src/android/support/design/widget/AppBarLayout.java b/design/src/android/support/design/widget/AppBarLayout.java
index 4c7ef0a..ff41db8 100644
--- a/design/src/android/support/design/widget/AppBarLayout.java
+++ b/design/src/android/support/design/widget/AppBarLayout.java
@@ -111,6 +111,7 @@
     static final int PENDING_ACTION_EXPANDED = 0x1;
     static final int PENDING_ACTION_COLLAPSED = 0x2;
     static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;
+    static final int PENDING_ACTION_FORCE = 0x8;
 
     /**
      * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
@@ -172,7 +173,7 @@
                 0, R.style.Widget_Design_AppBarLayout);
         ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
         if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
-            setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false));
+            setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false), false, false);
         }
         if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
             ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
@@ -299,8 +300,13 @@
      * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
      */
     public void setExpanded(boolean expanded, boolean animate) {
+        setExpanded(expanded, animate, true);
+    }
+
+    private void setExpanded(boolean expanded, boolean animate, boolean force) {
         mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
-                | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0);
+                | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
+                | (force ? PENDING_ACTION_FORCE : 0);
         requestLayout();
     }
 
@@ -408,8 +414,8 @@
                     // Only enter by the amount of the collapsed height
                     range += childHeight - ViewCompat.getMinimumHeight(child);
                 } else {
-                    // Else use the full height
-                    range += childHeight;
+                    // Else use the full height (minus the top inset)
+                    range += childHeight - getTopInset();
                 }
             } else if (range > 0) {
                 // If we've hit an non-quick return scrollable view, and we've already hit a
@@ -1048,8 +1054,21 @@
                 int layoutDirection) {
             boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
 
+            // The priority for for actions here is (first which is true wins):
+            // 1. forced pending actions
+            // 2. offsets for restorations
+            // 3. non-forced pending actions
             final int pendingAction = abl.getPendingAction();
-            if (pendingAction != PENDING_ACTION_NONE) {
+            if (mOffsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
+                View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
+                int offset = -child.getBottom();
+                if (mOffsetToChildIndexOnLayoutIsMinHeight) {
+                    offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
+                } else {
+                    offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
+                }
+                setHeaderTopBottomOffset(parent, abl, offset);
+            } else if (pendingAction != PENDING_ACTION_NONE) {
                 final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
                 if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
                     final int offset = -abl.getUpNestedPreScrollRange();
@@ -1065,15 +1084,6 @@
                         setHeaderTopBottomOffset(parent, abl, 0);
                     }
                 }
-            } else if (mOffsetToChildIndexOnLayout >= 0) {
-                View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
-                int offset = -child.getBottom();
-                if (mOffsetToChildIndexOnLayoutIsMinHeight) {
-                    offset += ViewCompat.getMinimumHeight(child);
-                } else {
-                    offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
-                }
-                setTopAndBottomOffset(offset);
             }
 
             // Finally reset any pending states
@@ -1085,6 +1095,11 @@
             setTopAndBottomOffset(
                     MathUtils.constrain(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
 
+            // Update the AppBarLayout's drawable state for any elevation changes.
+            // This is needed so that the elevation is set in the first layout, so that
+            // we don't get a visual elevation jump pre-N (due to the draw dispatch skip)
+            updateAppBarLayoutDrawableState(parent, abl, getTopAndBottomOffset(), 0, true);
+
             // Make sure we dispatch the offset update
             abl.dispatchOffsetUpdates(getTopAndBottomOffset());
 
@@ -1161,7 +1176,7 @@
 
                     // Update the AppBarLayout's drawable state (for any elevation changes)
                     updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
-                            newOffset < curOffset ? -1 : 1);
+                            newOffset < curOffset ? -1 : 1, false);
                 }
             } else {
                 // Reset the offset delta
@@ -1224,7 +1239,8 @@
         }
 
         private void updateAppBarLayoutDrawableState(final CoordinatorLayout parent,
-                final AppBarLayout layout, final int offset, final int direction) {
+                final AppBarLayout layout, final int offset, final int direction,
+                final boolean forceJump) {
             final View child = getAppBarChildOnOffset(layout, offset);
             if (child != null) {
                 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
@@ -1248,8 +1264,8 @@
 
                 final boolean changed = layout.setCollapsedState(collapsed);
 
-                if (changed && Build.VERSION.SDK_INT >= 11
-                        && shouldJumpElevationState(parent, layout)) {
+                if (Build.VERSION.SDK_INT >= 11 && (forceJump
+                        || (changed && shouldJumpElevationState(parent, layout)))) {
                     // If the collapsed state changed, we may need to
                     // jump to the current state if we have an overlapping view
                     layout.jumpDrawablesToCurrentState();
diff --git a/design/src/android/support/design/widget/BaseTransientBottomBar.java b/design/src/android/support/design/widget/BaseTransientBottomBar.java
index 864417d8..9035f827 100644
--- a/design/src/android/support/design/widget/BaseTransientBottomBar.java
+++ b/design/src/android/support/design/widget/BaseTransientBottomBar.java
@@ -28,7 +28,6 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 import android.support.design.R;
 import android.support.v4.view.ViewCompat;
@@ -418,12 +417,13 @@
                         switch (state) {
                             case SwipeDismissBehavior.STATE_DRAGGING:
                             case SwipeDismissBehavior.STATE_SETTLING:
-                                // If the view is being dragged or settling, cancel the timeout
-                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
+                                // If the view is being dragged or settling, pause the timeout
+                                SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
                                 break;
                             case SwipeDismissBehavior.STATE_IDLE:
                                 // If the view has been released and is idle, restore the timeout
-                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
+                                SnackbarManager.getInstance()
+                                        .restoreTimeoutIfPaused(mManagerCallback);
                                 break;
                         }
                     }
@@ -688,20 +688,20 @@
         @Override
         public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child,
                 MotionEvent event) {
-            // We want to make sure that we disable any Snackbar timeouts if the user is
-            // currently touching the Snackbar. We restore the timeout when complete
-            if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
-                switch (event.getActionMasked()) {
-                    case MotionEvent.ACTION_DOWN:
-                        SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
-                        break;
-                    case MotionEvent.ACTION_UP:
-                    case MotionEvent.ACTION_CANCEL:
-                        SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
-                        break;
-                }
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    // We want to make sure that we disable any Snackbar timeouts if the user is
+                    // currently touching the Snackbar. We restore the timeout when complete
+                    if (parent.isPointInChildBounds(child, (int) event.getX(),
+                            (int) event.getY())) {
+                        SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
+                    }
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);
+                    break;
             }
-
             return super.onInterceptTouchEvent(parent, child, event);
         }
     }
diff --git a/design/src/android/support/design/widget/BottomNavigationView.java b/design/src/android/support/design/widget/BottomNavigationView.java
index 3544d81..01ed8de 100644
--- a/design/src/android/support/design/widget/BottomNavigationView.java
+++ b/design/src/android/support/design/widget/BottomNavigationView.java
@@ -19,7 +19,11 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.support.annotation.DrawableRes;
+import android.support.annotation.IdRes;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.R;
@@ -27,6 +31,9 @@
 import android.support.design.internal.BottomNavigationMenuView;
 import android.support.design.internal.BottomNavigationPresenter;
 import android.support.v4.content.ContextCompat;
+import android.support.v4.os.ParcelableCompat;
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.view.AbsSavedState;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.content.res.AppCompatResources;
 import android.support.v7.view.SupportMenuInflater;
@@ -91,12 +98,15 @@
     private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
     private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
 
+    private static final int MENU_PRESENTER_ID = 1;
+
     private final MenuBuilder mMenu;
     private final BottomNavigationMenuView mMenuView;
     private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
     private MenuInflater mMenuInflater;
 
-    private OnNavigationItemSelectedListener mListener;
+    private OnNavigationItemSelectedListener mSelectedListener;
+    private OnNavigationItemReselectedListener mReselectedListener;
 
     public BottomNavigationView(Context context) {
         this(context, null);
@@ -121,6 +131,7 @@
         mMenuView.setLayoutParams(params);
 
         mPresenter.setBottomNavigationMenuView(mMenuView);
+        mPresenter.setId(MENU_PRESENTER_ID);
         mMenuView.setPresenter(mPresenter);
         mMenu.addMenuPresenter(mPresenter);
         mPresenter.initForMenu(getContext(), mMenu);
@@ -165,7 +176,12 @@
         mMenu.setCallback(new MenuBuilder.Callback() {
             @Override
             public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
-                return mListener != null && !mListener.onNavigationItemSelected(item);
+                if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
+                    mReselectedListener.onNavigationItemReselected(item);
+                    return true; // item is already selected
+                }
+                return mSelectedListener != null
+                        && !mSelectedListener.onNavigationItemSelected(item);
             }
 
             @Override
@@ -174,13 +190,30 @@
     }
 
     /**
-     * Set a listener that will be notified when a bottom navigation item is selected.
+     * Set a listener that will be notified when a bottom navigation item is selected. This listener
+     * will also be notified when the currently selected item is reselected, unless an
+     * {@link OnNavigationItemReselectedListener} has also been set.
      *
      * @param listener The listener to notify
+     *
+     * @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener)
      */
     public void setOnNavigationItemSelectedListener(
             @Nullable OnNavigationItemSelectedListener listener) {
-        mListener = listener;
+        mSelectedListener = listener;
+    }
+
+    /**
+     * Set a listener that will be notified when the currently selected bottom navigation item is
+     * reselected. This does not require an {@link OnNavigationItemSelectedListener} to be set.
+     *
+     * @param listener The listener to notify
+     *
+     * @see #setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener)
+     */
+    public void setOnNavigationItemReselectedListener(
+            @Nullable OnNavigationItemReselectedListener listener) {
+        mReselectedListener = listener;
     }
 
     /**
@@ -286,7 +319,33 @@
     }
 
     /**
-     * Listener for handling events on bottom navigation items.
+     * Returns the currently selected menu item ID, or zero if there is no menu.
+     *
+     * @see #setSelectedItemId(int)
+     */
+    @IdRes
+    public int getSelectedItemId() {
+        return mMenuView.getSelectedItemId();
+    }
+
+    /**
+     * Set the selected menu item ID. This behaves the same as tapping on an item.
+     *
+     * @param itemId The menu item ID. If no item has this ID, the current selection is unchanged.
+     *
+     * @see #getSelectedItemId()
+     */
+    public void setSelectedItemId(@IdRes int itemId) {
+        MenuItem item = mMenu.findItem(itemId);
+        if (item != null) {
+            if (!mMenu.performItemAction(item, mPresenter, 0)) {
+                item.setChecked(true);
+            }
+        }
+    }
+
+    /**
+     * Listener for handling selection events on bottom navigation items.
      */
     public interface OnNavigationItemSelectedListener {
 
@@ -302,6 +361,19 @@
         boolean onNavigationItemSelected(@NonNull MenuItem item);
     }
 
+    /**
+     * Listener for handling reselection events on bottom navigation items.
+     */
+    public interface OnNavigationItemReselectedListener {
+
+        /**
+         * Called when the currently selected item in the bottom navigation menu is selected again.
+         *
+         * @param item The selected item
+         */
+        void onNavigationItemReselected(@NonNull MenuItem item);
+    }
+
     private void addCompatibilityTopDivider(Context context) {
         View divider = new View(context);
         divider.setBackgroundColor(
@@ -344,4 +416,60 @@
                 defaultColor
         });
     }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState savedState = new SavedState(superState);
+        savedState.menuPresenterState = new Bundle();
+        mMenu.savePresenterStates(savedState.menuPresenterState);
+        return savedState;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+        SavedState savedState = (SavedState) state;
+        super.onRestoreInstanceState(savedState.getSuperState());
+        mMenu.restorePresenterStates(savedState.menuPresenterState);
+    }
+
+    static class SavedState extends AbsSavedState {
+        Bundle menuPresenterState;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public SavedState(Parcel source, ClassLoader loader) {
+            super(source, loader);
+            readFromParcel(source, loader);
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeBundle(menuPresenterState);
+        }
+
+        private void readFromParcel(Parcel in, ClassLoader loader) {
+            menuPresenterState = in.readBundle(loader);
+        }
+
+        public static final Creator<SavedState> CREATOR =
+                ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+                        return new SavedState(in, loader);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                });
+    }
 }
diff --git a/design/src/android/support/design/widget/BottomSheetDialog.java b/design/src/android/support/design/widget/BottomSheetDialog.java
index de48937..c5fccb0 100644
--- a/design/src/android/support/design/widget/BottomSheetDialog.java
+++ b/design/src/android/support/design/widget/BottomSheetDialog.java
@@ -97,6 +97,14 @@
     }
 
     @Override
+    protected void onStart() {
+        super.onStart();
+        if (mBehavior != null) {
+            mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+        }
+    }
+
+    @Override
     public void setCanceledOnTouchOutside(boolean cancel) {
         super.setCanceledOnTouchOutside(cancel);
         if (cancel && !mCancelable) {
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/design/src/android/support/design/widget/CoordinatorLayout.java
index bac67f9..e0ec883 100644
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ b/design/src/android/support/design/widget/CoordinatorLayout.java
@@ -1171,11 +1171,18 @@
     }
 
     /**
-     * Return the given gravity value or the default if the passed value is NO_GRAVITY.
-     * This should be used for children that are not anchored to another view or a keyline.
+     * Return the given gravity value, but if either or both of the axes doesn't have any gravity
+     * specified, the default value (start or top) is specified. This should be used for children
+     * that are not anchored to another view or a keyline.
      */
     private static int resolveGravity(int gravity) {
-        return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
+        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
+            gravity |= GravityCompat.START;
+        }
+        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
+            gravity |= Gravity.TOP;
+        }
+        return gravity;
     }
 
     /**
@@ -1293,7 +1300,7 @@
                 offsetChildByInset(child, inset, layoutDirection);
             }
 
-            if (type == EVENT_PRE_DRAW) {
+            if (type != EVENT_VIEW_REMOVED) {
                 // Did it change? if not continue
                 getLastChildRect(child, lastDrawRect);
                 if (lastDrawRect.equals(drawRect)) {
@@ -2560,8 +2567,10 @@
 
         /**
          * A {@link Gravity} value describing how this child view should lay out.
-         * If an {@link #setAnchorId(int) anchor} is also specified, the gravity describes
-         * how this child view should be positioned relative to its anchored position.
+         * If either or both of the axes are not specified, they are treated by CoordinatorLayout
+         * as {@link Gravity#TOP} or {@link GravityCompat#START}. If an
+         * {@link #setAnchorId(int) anchor} is also specified, the gravity describes how this child
+         * view should be positioned relative to its anchored position.
          */
         public int gravity = Gravity.NO_GRAVITY;
 
diff --git a/design/src/android/support/design/widget/Snackbar.java b/design/src/android/support/design/widget/Snackbar.java
index a096a3d..bd5ffba 100644
--- a/design/src/android/support/design/widget/Snackbar.java
+++ b/design/src/android/support/design/widget/Snackbar.java
@@ -133,6 +133,11 @@
     public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
             @Duration int duration) {
         final ViewGroup parent = findSuitableParent(view);
+        if (parent == null) {
+            throw new IllegalArgumentException("No suitable parent found from the given view. "
+                    + "Please provide a valid view.");
+        }
+
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
         final SnackbarContentLayout content =
                 (SnackbarContentLayout) inflater.inflate(
@@ -324,6 +329,25 @@
         public SnackbarLayout(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            // Work around our backwards-compatible refactoring of Snackbar and inner content
+            // being inflated against snackbar's parent (instead of against the snackbar itself).
+            // Every child that is width=MATCH_PARENT is remeasured again and given the full width
+            // minus the paddings.
+            int childCount = getChildCount();
+            int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                if (child.getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT) {
+                    child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY),
+                            MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
+                                    MeasureSpec.EXACTLY));
+                }
+            }
+        }
     }
 }
 
diff --git a/design/src/android/support/design/widget/SnackbarManager.java b/design/src/android/support/design/widget/SnackbarManager.java
index b391a3a..43892d3 100644
--- a/design/src/android/support/design/widget/SnackbarManager.java
+++ b/design/src/android/support/design/widget/SnackbarManager.java
@@ -137,17 +137,19 @@
         }
     }
 
-    public void cancelTimeout(Callback callback) {
+    public void pauseTimeout(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbarLocked(callback)) {
+            if (isCurrentSnackbarLocked(callback) && !mCurrentSnackbar.paused) {
+                mCurrentSnackbar.paused = true;
                 mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
             }
         }
     }
 
-    public void restoreTimeout(Callback callback) {
+    public void restoreTimeoutIfPaused(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbarLocked(callback)) {
+            if (isCurrentSnackbarLocked(callback) && mCurrentSnackbar.paused) {
+                mCurrentSnackbar.paused = false;
                 scheduleTimeoutLocked(mCurrentSnackbar);
             }
         }
@@ -168,6 +170,7 @@
     private static class SnackbarRecord {
         final WeakReference<Callback> callback;
         int duration;
+        boolean paused;
 
         SnackbarRecord(int duration, Callback callback) {
             this.callback = new WeakReference<>(callback);
diff --git a/design/src/android/support/design/widget/TabLayout.java b/design/src/android/support/design/widget/TabLayout.java
index b6f94a4..d3a8dd1 100755
--- a/design/src/android/support/design/widget/TabLayout.java
+++ b/design/src/android/support/design/widget/TabLayout.java
@@ -1084,17 +1084,7 @@
         final int targetScrollX = calculateScrollXForTab(newPosition, 0);
 
         if (startScrollX != targetScrollX) {
-            if (mScrollAnimator == null) {
-                mScrollAnimator = ViewUtils.createAnimator();
-                mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
-                mScrollAnimator.setDuration(ANIMATION_DURATION);
-                mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimatorCompat animator) {
-                        scrollTo(animator.getAnimatedIntValue(), 0);
-                    }
-                });
-            }
+            ensureScrollAnimator();
 
             mScrollAnimator.setIntValues(startScrollX, targetScrollX);
             mScrollAnimator.start();
@@ -1104,6 +1094,25 @@
         mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
     }
 
+    private void ensureScrollAnimator() {
+        if (mScrollAnimator == null) {
+            mScrollAnimator = ViewUtils.createAnimator();
+            mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+            mScrollAnimator.setDuration(ANIMATION_DURATION);
+            mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimatorCompat animator) {
+                    scrollTo(animator.getAnimatedIntValue(), 0);
+                }
+            });
+        }
+    }
+
+    void setScrollAnimatorListener(ValueAnimatorCompat.AnimatorListener listener) {
+        ensureScrollAnimator();
+        mScrollAnimator.addListener(listener);
+    }
+
     private void setSelectedTabView(int position) {
         final int tabCount = mTabStrip.getChildCount();
         if (position < tabCount) {
@@ -1177,10 +1186,14 @@
             final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
             final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
 
-            return selectedChild.getLeft()
-                    + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
-                    + (selectedChild.getWidth() / 2)
-                    - (getWidth() / 2);
+            // base scroll amount: places center of tab in center of parent
+            int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
+            // offset amount: fraction of the distance between centers of tabs
+            int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
+
+            return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
+                    ? scrollBase + scrollOffset
+                    : scrollBase - scrollOffset;
         }
         return 0;
     }
diff --git a/design/tests/AndroidManifest.xml b/design/tests/AndroidManifest.xml
index 29d46e0..886540d 100755
--- a/design/tests/AndroidManifest.xml
+++ b/design/tests/AndroidManifest.xml
@@ -88,6 +88,10 @@
         <activity
                 android:name="android.support.v7.app.AppCompatActivity"/>
 
+        <activity
+            android:name="android.support.design.widget.AppBarLayoutCollapsePinTestActivity"
+            android:theme="@style/Theme.TranslucentStatus"/>
+
     </application>
 
     <instrumentation
diff --git a/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml b/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml
new file mode 100644
index 0000000..fbe031b
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <android.support.design.widget.AppBarLayout
+        android:id="@+id/app_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/appbar_height"
+        android:fitsSystemWindows="true"
+        app:expanded="true">
+
+        <android.support.design.widget.CollapsingToolbarLayout
+            android:id="@+id/collapsing_app_bar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed">
+
+            <android.support.v7.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_height="?attr/actionBarSize"
+                android:layout_width="match_parent"
+                app:layout_collapseMode="pin"/>
+
+        </android.support.design.widget.CollapsingToolbarLayout>
+
+    </android.support.design.widget.AppBarLayout>
+
+    <include layout="@layout/include_appbar_scrollview" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/layout/design_appbar_toolbar_collapse_scroll_enteralways.xml b/design/tests/res/layout/design_appbar_toolbar_collapse_scroll_enteralways.xml
new file mode 100644
index 0000000..43a8d51
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_toolbar_collapse_scroll_enteralways.xml
@@ -0,0 +1,49 @@
+<?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.
+-->
+
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <android.support.design.widget.AppBarLayout
+        android:id="@+id/app_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/appbar_height"
+        android:fitsSystemWindows="true">
+
+        <android.support.design.widget.CollapsingToolbarLayout
+            android:id="@+id/collapsing_app_bar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_scrollFlags="scroll|enterAlways">
+
+            <android.support.v7.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_height="?attr/actionBarSize"
+                android:layout_width="match_parent"
+                app:layout_collapseMode="pin"/>
+
+        </android.support.design.widget.CollapsingToolbarLayout>
+
+    </android.support.design.widget.AppBarLayout>
+
+    <include layout="@layout/include_appbar_scrollview" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/layout/design_tabs_fixed_width.xml b/design/tests/res/layout/design_tabs_fixed_width.xml
new file mode 100644
index 0000000..752034f4
--- /dev/null
+++ b/design/tests/res/layout/design_tabs_fixed_width.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Width is fixed to test scrolling -->
+<android.support.design.widget.TabLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/tabs"
+    android:layout_width="160dp"
+    android:layout_height="wrap_content"
+    app:tabMode="scrollable">
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 0" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 1" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 2" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 3" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 4" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 5" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 6" />
+
+    <android.support.design.widget.TabItem
+        android:text="Tab 7" />
+
+</android.support.design.widget.TabLayout>
diff --git a/design/tests/res/menu/navigation_view_content.xml b/design/tests/res/menu/navigation_view_content.xml
index a8fb464..25fb016 100644
--- a/design/tests/res/menu/navigation_view_content.xml
+++ b/design/tests/res/menu/navigation_view_content.xml
@@ -29,7 +29,7 @@
               app:actionLayout="@layout/action_layout" />
         <item android:id="@+id/destination_settings"
               android:title="@string/navigate_settings" />
-        <item android:id="@+id/desitination_custom"
+        <item android:id="@+id/destination_custom"
               app:actionLayout="@layout/action_layout_custom" />
     </group>
 </menu>
diff --git a/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java b/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java
new file mode 100755
index 0000000..4d6fc63
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.testutils;
+
+import android.support.design.widget.AppBarLayout;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class AppBarLayoutMatchers {
+
+    /**
+     * Returns a matcher that matches AppBarLayouts which are collapsed.
+     */
+    public static Matcher isCollapsed() {
+        return new TypeSafeMatcher<AppBarLayout>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("AppBarLayout is collapsed");
+            }
+
+            @Override
+            protected boolean matchesSafely(AppBarLayout item) {
+                return item.getBottom() == (item.getHeight() - item.getTotalScrollRange());
+            }
+        };
+    }
+
+}
diff --git a/design/tests/src/android/support/design/testutils/NavigationViewActions.java b/design/tests/src/android/support/design/testutils/NavigationViewActions.java
index bcc15da8..6a05fab 100644
--- a/design/tests/src/android/support/design/testutils/NavigationViewActions.java
+++ b/design/tests/src/android/support/design/testutils/NavigationViewActions.java
@@ -16,6 +16,9 @@
 
 package android.support.design.testutils;
 
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+
 import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.DrawableRes;
@@ -29,9 +32,8 @@
 import android.support.test.espresso.ViewAction;
 import android.view.LayoutInflater;
 import android.view.View;
-import org.hamcrest.Matcher;
 
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import org.hamcrest.Matcher;
 
 public class NavigationViewActions {
     /**
@@ -305,4 +307,32 @@
             }
         };
     }
+
+    /**
+     * Removes the specified menu item from the navigation view.
+     *
+     * @param menuItemId The ID of the menu item to be removed.
+     */
+    public static ViewAction removeMenuItem(final @IdRes int menuItemId) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return isAssignableFrom(NavigationView.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "Remove menu item " + menuItemId;
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+                NavigationView navigationView = (NavigationView) view;
+                navigationView.getMenu().removeItem(menuItemId);
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
+
 }
diff --git a/design/tests/src/android/support/design/testutils/SwipeUtils.java b/design/tests/src/android/support/design/testutils/SwipeUtils.java
new file mode 100644
index 0000000..cf92883
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/SwipeUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.testutils;
+
+import android.support.test.espresso.action.CoordinatesProvider;
+import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
+import android.support.test.espresso.action.Swipe;
+import android.view.View;
+
+public class SwipeUtils {
+
+    public static GeneralSwipeAction swipeUp(final int swipeX,
+            final int swipeStartY, final int swipeAmountY) {
+        return new GeneralSwipeAction(
+                Swipe.SLOW,
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY };
+                    }
+                },
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY - swipeAmountY };
+                    }
+                },
+                Press.FINGER
+        );
+    }
+
+    public static GeneralSwipeAction swipeDown(final int swipeX,
+            final int swipeStartY, final int swipeAmountY) {
+        return new GeneralSwipeAction(
+                Swipe.SLOW,
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY };
+                    }
+                },
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY + swipeAmountY };
+                    }
+                },
+                Press.FINGER
+        );
+    }
+
+
+}
diff --git a/design/tests/src/android/support/design/testutils/TabLayoutActions.java b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
index 149b14f..7c17850 100644
--- a/design/tests/src/android/support/design/testutils/TabLayoutActions.java
+++ b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
@@ -16,23 +16,17 @@
 
 package android.support.design.testutils;
 
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+
 import android.support.annotation.Nullable;
 import android.support.design.widget.TabLayout;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
-import android.support.test.espresso.action.CoordinatesProvider;
-import android.support.test.espresso.action.GeneralClickAction;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Tap;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.PagerTitleStrip;
+import android.support.test.espresso.matcher.ViewMatchers;
 import android.support.v4.view.ViewPager;
 import android.view.View;
-import android.widget.TextView;
-import org.hamcrest.Matcher;
 
-import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import org.hamcrest.Matcher;
 
 public class TabLayoutActions {
     /**
@@ -143,4 +137,30 @@
             }
         };
     }
+
+    /**
+     * Calls <code>setScrollPosition(position, positionOffset, true)</code> on the
+     * <code>TabLayout</code>
+     */
+    public static ViewAction setScrollPosition(final int position, final float positionOffset) {
+        return new ViewAction() {
+
+            @Override
+            public Matcher<View> getConstraints() {
+                return ViewMatchers.isAssignableFrom(TabLayout.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "setScrollPosition(" + position + ", " + positionOffset + ", true)";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                TabLayout tabs = (TabLayout) view;
+                tabs.setScrollPosition(position, positionOffset, true);
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
 }
diff --git a/design/tests/src/android/support/design/testutils/TestUtils.java b/design/tests/src/android/support/design/testutils/TestUtils.java
index b9aae7a..2f9187d 100644
--- a/design/tests/src/android/support/design/testutils/TestUtils.java
+++ b/design/tests/src/android/support/design/testutils/TestUtils.java
@@ -16,7 +16,10 @@
 
 package android.support.design.testutils;
 
+import android.app.Activity;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -124,4 +127,20 @@
             }
         }
     }
+
+    /**
+     * Rotates the given Activity to either portrait or landscape, depending on the current
+     * orientation.
+     */
+    public static void rotateOrientation(@NonNull Activity activity) {
+        switch (activity.getResources().getConfiguration().orientation) {
+            case Configuration.ORIENTATION_PORTRAIT:
+                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+                break;
+            case Configuration.ORIENTATION_LANDSCAPE:
+            default:
+                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+                break;
+        }
+    }
 }
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/testutils/TestUtilsActions.java b/design/tests/src/android/support/design/testutils/TestUtilsActions.java
index b326039..a9d1233 100644
--- a/design/tests/src/android/support/design/testutils/TestUtilsActions.java
+++ b/design/tests/src/android/support/design/testutils/TestUtilsActions.java
@@ -24,8 +24,10 @@
 import android.graphics.drawable.Drawable;
 import android.os.Parcelable;
 import android.support.annotation.LayoutRes;
+import android.support.annotation.MenuRes;
 import android.support.annotation.Nullable;
 import android.support.design.widget.CollapsingToolbarLayout;
+import android.support.design.widget.NavigationView;
 import android.support.design.widget.TabLayout;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
@@ -339,4 +341,32 @@
         };
     }
 
+    /**
+     * Clears and inflates the menu.
+     *
+     * @param menuResId The menu resource XML to be used.
+     */
+    public static ViewAction reinflateMenu(final @MenuRes int menuResId) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return isAssignableFrom(NavigationView.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "clear and inflate menu " + menuResId;
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+                final NavigationView nv = (NavigationView) view;
+                nv.getMenu().clear();
+                nv.inflateMenu(menuResId);
+                uiController.loopMainThreadUntilIdle();
+            }
+        };
+    }
+
 }
diff --git a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
index 2f1f30d..36cdee6 100644
--- a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
+++ b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
@@ -18,18 +18,23 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
 import android.support.annotation.NonNull;
 import android.support.design.widget.FloatingActionButton;
 import android.support.test.espresso.matcher.BoundedMatcher;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.TextViewCompat;
+import android.support.v7.view.menu.MenuItemImpl;
 import android.view.Gravity;
+import android.view.Menu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
@@ -461,6 +466,24 @@
     }
 
     /**
+     * Returns a matcher that matches views which have a z-value greater than 0. Also matches if
+     * the platform we're running on does not support z-values.
+     */
+    public static Matcher<View> hasZ() {
+        return new TypeSafeMatcher<View>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("has a z value greater than 0");
+            }
+
+            @Override
+            public boolean matchesSafely(View view) {
+                return Build.VERSION.SDK_INT < 21 || ViewCompat.getZ(view) > 0f;
+            }
+        };
+    }
+
+    /**
      * Returns a matcher that matches TextViews with the specified typeface.
      */
     public static Matcher withTypeface(@NonNull final Typeface typeface) {
@@ -476,4 +499,36 @@
             }
         };
     }
+
+    /**
+     * Returns a matcher that matches the action view of the specified menu item.
+     *
+     * @param menu The menu
+     * @param id   The ID of the menu item
+     */
+    public static Matcher<View> isActionViewOf(@NonNull final Menu menu, @IdRes final int id) {
+        return new TypeSafeMatcher<View>() {
+
+            private Resources mResources;
+
+            @Override
+            protected boolean matchesSafely(View view) {
+                mResources = view.getResources();
+                MenuItemImpl item = (MenuItemImpl) menu.findItem(id);
+                return item != null && item.getActionView() == view;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                String name;
+                if (mResources != null) {
+                    name = mResources.getResourceName(id);
+                } else {
+                    name = Integer.toString(id);
+                }
+                description.appendText("is action view of menu item " + name);
+            }
+        };
+    }
+
 }
diff --git a/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java b/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
index fc131a4..bd49506 100644
--- a/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
@@ -17,6 +17,8 @@
 package android.support.design.widget;
 
 import static android.support.design.testutils.CollapsingToolbarLayoutActions.setContentScrimColor;
+import static android.support.design.testutils.SwipeUtils.swipeDown;
+import static android.support.design.testutils.SwipeUtils.swipeUp;
 import static android.support.design.testutils.TestUtilsActions.setText;
 import static android.support.design.testutils.TestUtilsActions.setTitle;
 import static android.support.test.espresso.Espresso.onView;
@@ -35,15 +37,10 @@
 import android.support.annotation.StringRes;
 import android.support.design.test.R;
 import android.support.design.testutils.Shakespeare;
-import android.support.test.espresso.action.CoordinatesProvider;
-import android.support.test.espresso.action.GeneralSwipeAction;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Swipe;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
-import android.view.View;
 import android.widget.TextView;
 
 import org.hamcrest.Description;
@@ -64,38 +61,12 @@
 
     protected static void performVerticalSwipeUpGesture(@IdRes int containerId, final int swipeX,
             final int swipeStartY, final int swipeAmountY) {
-        onView(withId(containerId)).perform(new GeneralSwipeAction(
-                Swipe.SLOW,
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY };
-                    }
-                },
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY - swipeAmountY };
-                    }
-                }, Press.FINGER));
+        onView(withId(containerId)).perform(swipeUp(swipeX, swipeStartY, swipeAmountY));
     }
 
     protected static void performVerticalSwipeDownGesture(@IdRes int containerId, final int swipeX,
             final int swipeStartY, final int swipeAmountY) {
-        onView(withId(containerId)).perform(new GeneralSwipeAction(
-                Swipe.SLOW,
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY };
-                    }
-                },
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY + swipeAmountY };
-                    }
-                }, Press.FINGER));
+        onView(withId(containerId)).perform(swipeDown(swipeX, swipeStartY, swipeAmountY));
     }
 
     @CallSuper
@@ -117,7 +88,13 @@
         });
 
         final CharSequence activityTitle = activity.getString(titleResId);
-        activity.setTitle(activityTitle);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                activity.setTitle(activityTitle);
+            }
+        });
+
         if (mCollapsingToolbar != null) {
             onView(withId(R.id.collapsing_app_bar))
                     .perform(setTitle(activityTitle))
diff --git a/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java b/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java
new file mode 100644
index 0000000..38ea4fc
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.support.design.test.R;
+import android.support.v7.widget.Toolbar;
+
+public class AppBarLayoutCollapsePinTestActivity extends BaseTestActivity {
+
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.design_appbar_toolbar_collapse_pin_restore_test;
+    }
+
+    @Override
+    protected void onContentViewSet() {
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+    }
+}
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
new file mode 100644
index 0000000..52f4ab2
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import static android.support.design.testutils.AppBarLayoutMatchers.isCollapsed;
+import static android.support.design.testutils.SwipeUtils.swipeUp;
+import static android.support.design.testutils.TestUtils.rotateOrientation;
+import static android.support.design.testutils.TestUtilsMatchers.hasZ;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+import android.app.Activity;
+import android.support.design.test.R;
+
+import org.junit.Test;
+
+public class AppBarWithCollapsingToolbarStateRestoreTest
+        extends BaseInstrumentationTestCase<AppBarLayoutCollapsePinTestActivity> {
+
+    public AppBarWithCollapsingToolbarStateRestoreTest() {
+        super(AppBarLayoutCollapsePinTestActivity.class);
+    }
+
+    @Test
+    public void testRotateAndRestore() {
+        Activity activity = mActivityTestRule.getActivity();
+        final AppBarLayout appBar = (AppBarLayout) activity.findViewById(R.id.app_bar);
+
+        // Swipe up and collapse the AppBarLayout
+        onView(withId(R.id.coordinator_layout))
+                .perform(swipeUp(
+                        appBar.getLeft() + (appBar.getWidth() / 2),
+                        appBar.getBottom() + 20,
+                        appBar.getHeight()));
+        onView(withId(R.id.app_bar))
+                .check(matches(hasZ()))
+                .check(matches(isCollapsed()));
+
+        // Now rotate the Activity
+        rotateOrientation(activity);
+
+        // And check that the app bar still is restored correctly
+        onView(withId(R.id.app_bar))
+                .check(matches(hasZ()))
+                .check(matches(isCollapsed()));
+    }
+
+}
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
index adcfee8..aeca0be 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
@@ -16,19 +16,26 @@
 
 package android.support.design.widget;
 
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.greaterThan;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 
 import android.os.Build;
 import android.os.SystemClock;
 import android.support.design.test.R;
+import android.support.test.filters.FlakyTest;
 import android.support.test.filters.LargeTest;
 import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.Suppress;
 import android.widget.ImageView;
 
 import org.junit.Test;
 
 @LargeTest
 public class AppBarWithCollapsingToolbarTest extends AppBarLayoutBaseTest {
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbar() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_pin,
@@ -134,6 +141,8 @@
         assertScrimAlpha(0);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testScrollingToolbar() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_scroll,
@@ -244,6 +253,115 @@
         assertScrimAlpha(0);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
+    @Test
+    public void testScrollingToolbarEnterAlways() throws Throwable {
+        configureContent(R.layout.design_appbar_toolbar_collapse_scroll_enteralways,
+                R.string.design_appbar_collapsing_toolbar_scroll);
+
+        final int[] appbarOnScreenXY = new int[2];
+        final int[] coordinatorLayoutOnScreenXY = new int[2];
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+        mCoordinatorLayout.getLocationOnScreen(coordinatorLayoutOnScreenXY);
+
+        final int topInset = mAppBar.getTopInset();
+
+        final int originalAppbarTop = appbarOnScreenXY[1];
+        final int originalAppbarBottom = appbarOnScreenXY[1] + mAppBar.getHeight();
+        final int centerX = appbarOnScreenXY[0] + mAppBar.getWidth() / 2;
+
+        final int toolbarHeight = mToolbar.getHeight();
+        final int appbarHeight = mAppBar.getHeight();
+        final int longSwipeAmount = 3 * appbarHeight / 2;
+        final int reallyLongSwipeAmount = 2 * appbarHeight;
+        final int shortSwipeAmount = toolbarHeight;
+
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(0);
+
+        // Perform a swipe-up gesture across the horizontal center of the screen, starting from
+        // just below the AppBarLayout
+        performVerticalSwipeUpGesture(
+                R.id.coordinator_layout,
+                centerX,
+                originalAppbarBottom + 20,
+                longSwipeAmount);
+
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+        // At this point the app bar should not be visually "present" on the screen, with its bottom
+        // edge aligned with the bottom of system status bar. If we're running on a device which
+        // supports a translucent status bar, we need to take the status bar height into account.
+        // Allow for off-by-a-pixel margin of error.
+        assertEquals(originalAppbarTop, appbarOnScreenXY[1] + appbarHeight - topInset, 1);
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(255);
+
+        // Perform another swipe-up gesture
+        performVerticalSwipeUpGesture(
+                R.id.coordinator_layout,
+                centerX,
+                originalAppbarBottom,
+                shortSwipeAmount);
+
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+        // At this point the app bar should still be off the screen. Allow for off-by-a-pixel
+        // margin of error.
+        assertEquals(originalAppbarTop, appbarOnScreenXY[1] + appbarHeight - topInset, 1);
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(255);
+
+        // Perform a short swipe-down gesture across the horizontal center of the screen.
+        // Note that the swipe down is a bit longer than the swipe up to fully bring down
+        // the scrolled-away toolbar
+        performVerticalSwipeDownGesture(
+                R.id.coordinator_layout,
+                centerX,
+                originalAppbarBottom,
+                3 * shortSwipeAmount / 2);
+
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+
+        // At this point the app bar should be visually below the system status bar as it
+        // in scrolling mode and we've swiped down, not fully but more than collapsed
+        assertThat(appbarOnScreenXY[1] + appbarHeight,
+                is(greaterThan(originalAppbarTop + toolbarHeight + topInset)));
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(255);
+
+        // Perform another swipe-down gesture across the horizontal center of the screen.
+        performVerticalSwipeDownGesture(
+                R.id.coordinator_layout,
+                centerX,
+                originalAppbarBottom,
+                reallyLongSwipeAmount);
+
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+        // At this point the app bar should be in its original position.
+        // Allow for off-by-a-pixel margin of error.
+        assertEquals(originalAppbarTop, appbarOnScreenXY[1]);
+        assertEquals(originalAppbarBottom, appbarOnScreenXY[1] + appbarHeight);
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(0);
+
+        // Perform yet another swipe-down gesture across the horizontal center of the screen.
+        performVerticalSwipeDownGesture(
+                R.id.coordinator_layout,
+                centerX,
+                originalAppbarBottom,
+                longSwipeAmount);
+
+        mAppBar.getLocationOnScreen(appbarOnScreenXY);
+        // At this point the app bar should still be in its original position.
+        // Allow for off-by-a-pixel margin of error.
+        assertEquals(originalAppbarTop, appbarOnScreenXY[1], 1);
+        assertEquals(originalAppbarBottom, appbarOnScreenXY[1] + appbarHeight, 1);
+        assertAppBarElevation(mDefaultElevationValue);
+        assertScrimAlpha(0);
+    }
+
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbarAndAnchoredFab() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_pin_with_fab,
@@ -310,6 +428,8 @@
         }
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbarAndParallaxImage() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_with_image,
@@ -413,6 +533,8 @@
      * inherits from) has an issue with measuring children with margins when run on earlier
      * versions of the platform.
      */
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     @SdkSuppress(minSdkVersion = 11)
     public void testPinnedToolbarWithMargins() throws Throwable {
diff --git a/design/tests/src/android/support/design/widget/AppBarWithToolbarAndTabsTest.java b/design/tests/src/android/support/design/widget/AppBarWithToolbarAndTabsTest.java
index c745f17..23ef7c5 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithToolbarAndTabsTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithToolbarAndTabsTest.java
@@ -27,7 +27,9 @@
 import android.support.annotation.StringRes;
 import android.support.design.test.R;
 import android.support.design.testutils.Cheeses;
+import android.support.test.filters.FlakyTest;
 import android.support.test.filters.LargeTest;
+import android.support.test.filters.Suppress;
 
 import org.junit.Test;
 
@@ -232,6 +234,8 @@
         assertAppBarElevation(mDefaultElevationValue);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @LargeTest
     @Test
     public void testSnappingToolbarAndSnappingTabs() throws Throwable {
diff --git a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
index f06a85a..5a536f6 100644
--- a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
@@ -32,12 +32,14 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Resources;
+import android.os.Parcelable;
 import android.support.annotation.ColorInt;
 import android.support.design.test.R;
 import android.support.design.testutils.TestDrawable;
@@ -124,6 +126,15 @@
         // Verify the item is now selected
         assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
 
+        // Select the same item again
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify our listener has been notified of the click
+        verify(mockedListener, times(2)).onNavigationItemSelected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_profile));
+        // Verify the item is still selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+
         // Make the listener return false to disallow selecting the item.
         when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false);
         onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)),
@@ -142,12 +153,151 @@
         // Click one of our items
         onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)),
                 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
-        // And that our previous listener has not been notified of the click
+        // Verify that our previous listener has not been notified of the click
         verifyNoMoreInteractions(mockedListener);
         // Verify the correct item is now selected.
         assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked());
     }
 
+    @UiThreadTest
+    @Test
+    @SmallTest
+    public void testSetSelectedItemId() {
+        BottomNavigationView.OnNavigationItemSelectedListener mockedListener =
+                mock(BottomNavigationView.OnNavigationItemSelectedListener.class);
+        mBottomNavigation.setOnNavigationItemSelectedListener(mockedListener);
+
+        // Make the listener return true to allow selecting the item.
+        when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true);
+        // Programmatically select an item
+        mBottomNavigation.setSelectedItemId(R.id.destination_profile);
+        // Verify our listener has been notified of the click
+        verify(mockedListener, times(1)).onNavigationItemSelected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_profile));
+        // Verify the item is now selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+
+        // Select the same item
+        mBottomNavigation.setSelectedItemId(R.id.destination_profile);
+        // Verify our listener has been notified of the click
+        verify(mockedListener, times(2)).onNavigationItemSelected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_profile));
+        // Verify the item is still selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+
+        // Make the listener return false to disallow selecting the item.
+        when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false);
+        // Programmatically select an item
+        mBottomNavigation.setSelectedItemId(R.id.destination_people);
+        // Verify our listener has been notified of the click
+        verify(mockedListener, times(1)).onNavigationItemSelected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_people));
+        // Verify the previous item is still selected
+        assertFalse(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+
+        // Set null listener to test that the next click is not going to notify the
+        // previously set listener and will allow selecting items.
+        mBottomNavigation.setOnNavigationItemSelectedListener(null);
+
+        // Select one of our items
+        mBottomNavigation.setSelectedItemId(R.id.destination_home);
+        // Verify that our previous listener has not been notified of the click
+        verifyNoMoreInteractions(mockedListener);
+        // Verify the correct item is now selected.
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked());
+    }
+
+    @Test
+    @SmallTest
+    public void testNavigationReselectionListener() {
+        // Add an OnNavigationItemReselectedListener
+        BottomNavigationView.OnNavigationItemReselectedListener reselectedListener =
+                mock(BottomNavigationView.OnNavigationItemReselectedListener.class);
+        mBottomNavigation.setOnNavigationItemReselectedListener(reselectedListener);
+
+        // Select an item
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify the item is now selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+        // Verify the listener was not called
+        verify(reselectedListener, never()).onNavigationItemReselected(any(MenuItem.class));
+
+        // Select the same item again
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify the item is still selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+        // Verify the listener was called
+        verify(reselectedListener, times(1)).onNavigationItemReselected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_profile));
+
+        // Add an OnNavigationItemSelectedListener
+        BottomNavigationView.OnNavigationItemSelectedListener selectedListener =
+                mock(BottomNavigationView.OnNavigationItemSelectedListener.class);
+        mBottomNavigation.setOnNavigationItemSelectedListener(selectedListener);
+        // Make the listener return true to allow selecting the item.
+        when(selectedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true);
+
+        // Select another item
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify the item is now selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
+        // Verify the correct listeners were called
+        verify(selectedListener, times(1)).onNavigationItemSelected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_people));
+        verify(reselectedListener, never()).onNavigationItemReselected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_people));
+
+        // Select the same item again
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify the item is still selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
+        // Verify the correct listeners were called
+        verifyNoMoreInteractions(selectedListener);
+        verify(reselectedListener, times(1)).onNavigationItemReselected(
+                mBottomNavigation.getMenu().findItem(R.id.destination_people));
+
+        // Remove the OnNavigationItemReselectedListener
+        mBottomNavigation.setOnNavigationItemReselectedListener(null);
+
+        // Select the same item again
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        // Verify the item is still selected
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
+        // Verify the reselectedListener was not called
+        verifyNoMoreInteractions(reselectedListener);
+    }
+
+    @UiThreadTest
+    @Test
+    @SmallTest
+    public void testSelectedItemIdWithEmptyMenu() {
+        // First item initially selected
+        assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId());
+
+        // Remove all the items
+        for (int id : mMenuStringContent.keySet()) {
+            mBottomNavigation.getMenu().removeItem(id);
+        }
+        // Verify selected ID is zero
+        assertEquals(0, mBottomNavigation.getSelectedItemId());
+
+        // Add an item
+        mBottomNavigation.getMenu().add(0, R.id.destination_home, 0, R.string.navigate_home);
+        // Verify item is selected
+        assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId());
+
+        // Try selecting an invalid ID
+        mBottomNavigation.setSelectedItemId(R.id.destination_people);
+        // Verify the view has not changed
+        assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId());
+    }
+
     @Test
     @SmallTest
     public void testIconTinting() {
@@ -233,6 +383,29 @@
         assertEquals(3, mBottomNavigation.getMenu().size());
     }
 
+    @Test
+    @SmallTest
+    public void testSavedState() throws Throwable {
+        // Select an item other than the first
+        onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)),
+                isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click());
+        assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
+        // Save the state
+        final Parcelable state = mBottomNavigation.onSaveInstanceState();
+
+        // Restore the state into a fresh BottomNavigationView
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                BottomNavigationView testView =
+                        new BottomNavigationView(mActivityTestRule.getActivity());
+                testView.inflateMenu(R.menu.bottom_navigation_view_content);
+                testView.onRestoreInstanceState(state);
+                assertTrue(testView.getMenu().findItem(R.id.destination_profile).isChecked());
+            }
+        });
+    }
+
     private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable {
         menu.findItem(id).setChecked(true);
         for (int i = 0; i < menu.size(); i++) {
diff --git a/design/tests/src/android/support/design/widget/BottomSheetDialogTest.java b/design/tests/src/android/support/design/widget/BottomSheetDialogTest.java
index 84c522c..4c6bbbf 100644
--- a/design/tests/src/android/support/design/widget/BottomSheetDialogTest.java
+++ b/design/tests/src/android/support/design/widget/BottomSheetDialogTest.java
@@ -21,6 +21,11 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.lessThan;
 import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.content.DialogInterface;
@@ -171,6 +176,40 @@
         }
     }
 
+    @SuppressWarnings("WrongConstant")
+    @Test
+    @MediumTest
+    public void testHideThenShow() throws Throwable {
+        // Hide the bottom sheet and wait for the dialog to be canceled.
+        final DialogInterface.OnCancelListener onCancelListener = mock(
+                DialogInterface.OnCancelListener.class);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                showDialog();
+                mDialog.setOnCancelListener(onCancelListener);
+            }
+        });
+        Espresso.onView(ViewMatchers.withId(R.id.design_bottom_sheet))
+                .perform(setState(BottomSheetBehavior.STATE_HIDDEN));
+        verify(onCancelListener, timeout(3000)).onCancel(any(DialogInterface.class));
+        // Reshow the same dialog instance and wait for the bottom sheet to be collapsed.
+        final BottomSheetBehavior.BottomSheetCallback callback = mock(
+                BottomSheetBehavior.BottomSheetCallback.class);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                BottomSheetBehavior.from(mDialog.findViewById(R.id.design_bottom_sheet))
+                        .setBottomSheetCallback(callback);
+                mDialog.show(); // Show the same dialog again.
+            }
+        });
+        verify(callback, timeout(3000)).onStateChanged(any(View.class),
+                eq(BottomSheetBehavior.STATE_SETTLING));
+        verify(callback, timeout(3000)).onStateChanged(any(View.class),
+                eq(BottomSheetBehavior.STATE_COLLAPSED));
+    }
+
     private void showDialog() {
         Context context = mActivityTestRule.getActivity();
         mDialog = new BottomSheetDialog(context);
diff --git a/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java b/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
index 1d4f0a7..73ad193 100644
--- a/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
@@ -16,6 +16,7 @@
 
 package android.support.design.widget;
 
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.action.ViewActions.swipeUp;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
@@ -40,10 +41,11 @@
 import android.graphics.Rect;
 import android.support.design.test.R;
 import android.support.design.testutils.CoordinatorLayoutUtils;
+import android.support.design.testutils.CoordinatorLayoutUtils.DependentBehavior;
 import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
+import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.WindowInsetsCompat;
 import android.view.Gravity;
@@ -69,13 +71,13 @@
 
     @Before
     public void setup() {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mInstrumentation = getInstrumentation();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 21)
     public void testSetFitSystemWindows() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
         final View view = new View(col.getContext());
 
@@ -128,6 +130,58 @@
     }
 
     @Test
+    public void testLayoutChildren() throws Throwable {
+        final Instrumentation instrumentation = getInstrumentation();
+        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
+        final View view = new View(col.getContext());
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                col.addView(view, 100, 100);
+            }
+        });
+        instrumentation.waitForIdleSync();
+        int horizontallyCentered = (col.getWidth() - view.getWidth()) / 2;
+        int end = col.getWidth() - view.getWidth();
+        int verticallyCentered = (col.getHeight() - view.getHeight()) / 2;
+        int bottom = col.getHeight() - view.getHeight();
+        final int[][] testCases = {
+                // gravity, expected left, expected top
+                {Gravity.NO_GRAVITY, 0, 0},
+                {Gravity.LEFT, 0, 0},
+                {GravityCompat.START, 0, 0},
+                {Gravity.TOP, 0, 0},
+                {Gravity.CENTER, horizontallyCentered, verticallyCentered},
+                {Gravity.CENTER_HORIZONTAL, horizontallyCentered, 0},
+                {Gravity.CENTER_VERTICAL, 0, verticallyCentered},
+                {Gravity.RIGHT, end, 0},
+                {GravityCompat.END, end, 0},
+                {Gravity.BOTTOM, 0, bottom},
+                {Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, horizontallyCentered, bottom},
+                {Gravity.RIGHT | Gravity.CENTER_VERTICAL, end, verticallyCentered},
+        };
+        for (final int[] testCase : testCases) {
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    final CoordinatorLayout.LayoutParams lp =
+                            (CoordinatorLayout.LayoutParams) view.getLayoutParams();
+                    lp.gravity = testCase[0];
+                    view.setLayoutParams(lp);
+                }
+            });
+            instrumentation.waitForIdleSync();
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    assertThat("Gravity: " + testCase[0], view.getLeft(), is(testCase[1]));
+                    assertThat("Gravity: " + testCase[0], view.getTop(), is(testCase[2]));
+                }
+            });
+        }
+    }
+
+    @Test
     public void testInsetDependency() {
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
@@ -183,7 +237,7 @@
 
     @Test
     public void testInsetEdge() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         final View insetView = new View(col.getContext());
@@ -236,7 +290,7 @@
 
     @Test
     public void testDependentViewChanged() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -250,7 +304,7 @@
         lpB.width = 100;
         lpB.height = 100;
         final CoordinatorLayout.Behavior behavior =
-                spy(new CoordinatorLayoutUtils.DependentBehavior(viewA));
+                spy(new DependentBehavior(viewA));
         lpB.setBehavior(behavior);
 
         mActivityTestRule.runOnUiThread(new Runnable() {
@@ -282,7 +336,7 @@
 
     @Test
     public void testDependentViewRemoved() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -290,7 +344,7 @@
         final View viewB = new View(col.getContext());
         final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
         final CoordinatorLayout.Behavior behavior =
-                spy(new CoordinatorLayoutUtils.DependentBehavior(viewA));
+                spy(new DependentBehavior(viewA));
         lpB.setBehavior(behavior);
 
         mActivityTestRule.runOnUiThread(new Runnable() {
@@ -316,7 +370,7 @@
 
     @Test
     public void testGetDependenciesAfterDependentViewRemoved() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -572,6 +626,55 @@
     }
 
     @Test
+    public void testNestedScrollingTriggeringDependentViewChanged() throws Throwable {
+        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
+        final CoordinatorLayout col = activity.mCoordinatorLayout;
+
+        // First a NestedScrollView to trigger nested scrolling
+        final View scrollView = LayoutInflater.from(activity).inflate(
+                R.layout.include_nestedscrollview, col, false);
+
+        // Now create a View and Behavior which depend on the scrollview
+        final ImageView dependentView = new ImageView(activity);
+        final CoordinatorLayout.Behavior dependentBehavior = spy(new DependentBehavior(scrollView));
+
+        // Finally a view which accepts nested scrolling in the CoordinatorLayout
+        final ImageView nestedScrollAwareView = new ImageView(activity);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // First add the ScrollView
+                col.addView(scrollView);
+
+                // Now add the view which depends on the scrollview
+                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
+                clp.setBehavior(dependentBehavior);
+                col.addView(dependentView, clp);
+
+                // Now add the nested scrolling aware view
+                clp = new CoordinatorLayout.LayoutParams(200, 200);
+                clp.setBehavior(new NestedScrollingBehavior());
+                col.addView(nestedScrollAwareView, clp);
+            }
+        });
+
+        // Wait for any layouts, and reset the Behavior so that the call counts are 0
+        getInstrumentation().waitForIdleSync();
+        reset(dependentBehavior);
+
+        // Now vertically swipe up on the NSV, causing nested scrolling to occur
+        onView(withId(R.id.nested_scrollview)).perform(swipeUp());
+
+        // Verify that the Behavior's onDependentViewChanged is not called due to the
+        // nested scroll
+        verify(dependentBehavior, never()).onDependentViewChanged(
+                eq(col), // parent
+                eq(dependentView), // child
+                eq(scrollView)); // axes
+    }
+
+    @Test
     public void testDodgeInsetViewWithEmptyBounds() throws Throwable {
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
@@ -614,9 +717,9 @@
                 .getInsetDodgeRect(same(col), same(view), any(Rect.class));
     }
 
-    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<ImageView> {
+    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<View> {
         @Override
-        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, ImageView child,
+        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                 View directTargetChild, View target, int nestedScrollAxes) {
             // Return true so that we always accept nested scroll events
             return true;
diff --git a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
index 069055b..e8cc701 100644
--- a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
+++ b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
@@ -33,11 +33,15 @@
 import static android.support.design.testutils.TestUtilsMatchers.withFabContentHeight;
 import static android.support.design.widget.DesignViewActions.setVisibility;
 import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
 import static org.hamcrest.Matchers.not;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
 import android.content.res.ColorStateList;
@@ -229,6 +233,19 @@
     }
 
     @Test
+    public void testOnClickListener() {
+        final View.OnClickListener listener = mock(View.OnClickListener.class);
+        final View view = mActivityTestRule.getActivity().findViewById(R.id.fab_standard);
+        view.setOnClickListener(listener);
+
+        // Click on the fab
+        onView(withId(R.id.fab_standard)).perform(click());
+
+        // And verify that the listener was invoked once
+        verify(listener, times(1)).onClick(view);
+    }
+
+    @Test
     public void testSetCompatElevation() {
         onView(withId(R.id.fab_standard))
                 .perform(setEnabled(false))
diff --git a/design/tests/src/android/support/design/widget/NavigationViewTest.java b/design/tests/src/android/support/design/widget/NavigationViewTest.java
index f0c064e..8d6746d 100755
--- a/design/tests/src/android/support/design/widget/NavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/NavigationViewTest.java
@@ -20,6 +20,7 @@
 import static android.support.design.testutils.NavigationViewActions.addHeaderView;
 import static android.support.design.testutils.NavigationViewActions.inflateHeaderView;
 import static android.support.design.testutils.NavigationViewActions.removeHeaderView;
+import static android.support.design.testutils.NavigationViewActions.removeMenuItem;
 import static android.support.design.testutils.NavigationViewActions.setCheckedItem;
 import static android.support.design.testutils.NavigationViewActions.setIconForMenuItem;
 import static android.support.design.testutils.NavigationViewActions.setItemBackground;
@@ -27,7 +28,9 @@
 import static android.support.design.testutils.NavigationViewActions.setItemIconTintList;
 import static android.support.design.testutils.NavigationViewActions.setItemTextAppearance;
 import static android.support.design.testutils.NavigationViewActions.setItemTextColor;
+import static android.support.design.testutils.TestUtilsActions.reinflateMenu;
 import static android.support.design.testutils.TestUtilsActions.restoreHierarchyState;
+import static android.support.design.testutils.TestUtilsMatchers.isActionViewOf;
 import static android.support.design.testutils.TestUtilsMatchers.isChildOfA;
 import static android.support.design.testutils.TestUtilsMatchers.withBackgroundFill;
 import static android.support.design.testutils.TestUtilsMatchers.withStartDrawableFilledWith;
@@ -66,6 +69,7 @@
 import android.support.design.test.R;
 import android.support.design.testutils.TestDrawable;
 import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
@@ -401,6 +405,7 @@
         verifyHeaders(R.id.header2, R.id.header3, R.id.header3);
     }
 
+    @SdkSuppress(minSdkVersion = 11)
     @Test
     public void testHeaderState() {
         // Open our drawer
@@ -439,6 +444,38 @@
                 .check(matches(isChecked()));
     }
 
+    @SdkSuppress(minSdkVersion = 11)
+    @Test
+    public void testActionViewState() {
+        // Open our drawer
+        onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START));
+
+        final Menu menu = mNavigationView.getMenu();
+        onView(isActionViewOf(menu, R.id.destination_people))
+                .check(matches(isNotChecked())) // Not checked by default
+                .perform(click())               // Check it
+                .check(matches(isChecked()));
+
+        // Remove the other action view to simulate the case where it is not yet inflated
+        onView(isActionViewOf(menu, R.id.destination_custom))
+                .check(matches(isDisplayed()));
+        onView(withId(R.id.start_drawer))
+                .perform(removeMenuItem(R.id.destination_custom));
+
+        // Save the current state
+        SparseArray<Parcelable> container = new SparseArray<>();
+        mNavigationView.saveHierarchyState(container);
+
+        // Restore the saved state
+        onView(withId(R.id.start_drawer))
+                .perform(reinflateMenu(R.menu.navigation_view_content))
+                .perform(restoreHierarchyState(container));
+
+        // Checked state should be restored
+        onView(isActionViewOf(menu, R.id.destination_people))
+                .check(matches(isChecked()));
+    }
+
     @Test
     public void testNavigationSelectionListener() {
         // Open our drawer
diff --git a/design/tests/src/android/support/design/widget/SnackbarTest.java b/design/tests/src/android/support/design/widget/SnackbarTest.java
index 23092e8..5f5682c 100644
--- a/design/tests/src/android/support/design/widget/SnackbarTest.java
+++ b/design/tests/src/android/support/design/widget/SnackbarTest.java
@@ -46,6 +46,10 @@
 import android.support.design.testutils.SnackbarUtils;
 import android.support.test.espresso.ViewAction;
 import android.support.test.espresso.ViewInteraction;
+import android.support.test.espresso.action.CoordinatesProvider;
+import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
+import android.support.test.espresso.action.Swipe;
 import android.support.test.filters.MediumTest;
 import android.support.v4.view.ViewCompat;
 import android.text.TextUtils;
@@ -242,6 +246,41 @@
     }
 
     @Test
+    public void testSwipeUpDismissesViaTimeout() throws Throwable {
+        verifyDismissCallback(
+                onView(isAssignableFrom(Snackbar.SnackbarLayout.class)),
+                // This is a swipe up, from the middle center of the view, to above the view
+                // (outside the bounds)
+                new GeneralSwipeAction(
+                        Swipe.SLOW,
+                        new CoordinatesProvider() {
+                            @Override
+                            public float[] calculateCoordinates(View view) {
+                                final int[] loc = new int[2];
+                                view.getLocationOnScreen(loc);
+                                return new float[]{
+                                        loc[0] + view.getWidth() / 2,
+                                        loc[1] + view.getHeight() / 2};
+                            }
+                        },
+                        new CoordinatesProvider() {
+                            @Override
+                            public float[] calculateCoordinates(View view) {
+                                final int[] loc = new int[2];
+                                view.getLocationOnScreen(loc);
+                                return new float[]{
+                                        loc[0] + view.getWidth() / 2,
+                                        loc[1] - view.getHeight()};
+                            }
+                        },
+                        Press.FINGER
+                ),
+                null,
+                Snackbar.LENGTH_SHORT,
+                Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
+    }
+
+    @Test
     public void testDismissViaAnotherSnackbar() throws Throwable {
         final Snackbar anotherSnackbar =
                 Snackbar.make(mCoordinatorLayout, "A different message", Snackbar.LENGTH_SHORT);
diff --git a/design/tests/src/android/support/design/widget/TabLayoutTest.java b/design/tests/src/android/support/design/widget/TabLayoutTest.java
index 539ab86..e9255fa 100755
--- a/design/tests/src/android/support/design/widget/TabLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutTest.java
@@ -16,6 +16,12 @@
 
 package android.support.design.widget;
 
+import static android.support.design.testutils.TabLayoutActions.selectTab;
+import static android.support.design.testutils.TabLayoutActions.setScrollPosition;
+import static android.support.design.testutils.TestUtilsActions.setLayoutDirection;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -29,7 +35,12 @@
 
 import android.support.design.test.R;
 import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.Espresso;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.NoMatchingViewException;
+import android.support.test.espresso.ViewAssertion;
 import android.support.test.filters.SmallTest;
+import android.support.v4.view.ViewCompat;
 import android.support.v7.app.AppCompatActivity;
 import android.view.InflateException;
 import android.view.LayoutInflater;
@@ -37,6 +48,8 @@
 
 import org.junit.Test;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 @SmallTest
 public class TabLayoutTest extends BaseInstrumentationTestCase<AppCompatActivity> {
     public TabLayoutTest() {
@@ -188,4 +201,105 @@
             }
         }
     }
+
+    @Test
+    public void setScrollPositionLtr() throws Throwable {
+        testSetScrollPosition(true);
+    }
+
+    @Test
+    public void setScrollPositionRtl() throws Throwable {
+        testSetScrollPosition(false);
+    }
+
+    private void testSetScrollPosition(final boolean isLtr) throws Throwable {
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width);
+            }
+        });
+        final TabLayout tabs = (TabLayout) mActivityTestRule.getActivity().findViewById(R.id.tabs);
+        assertEquals(TabLayout.MODE_SCROLLABLE, tabs.getTabMode());
+
+        final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs);
+        Espresso.registerIdlingResources(idler);
+
+        // We're going to call setScrollPosition() incrementally, as if scrolling between one tab
+        // and the next. Use the middle tab for best results. The positionOffsets should be in the
+        // range [0, 1), so the final call will wrap to 0 but use the next tab's position.
+        final int middleTab = tabs.getTabCount() / 2;
+        final int[] positions = {middleTab, middleTab, middleTab, middleTab, middleTab + 1};
+        final float[] positionOffsets = {0f, .25f, .5f, .75f, 0f};
+
+        // Set layout direction
+        onView(withId(R.id.tabs)).perform(setLayoutDirection(
+                isLtr ? ViewCompat.LAYOUT_DIRECTION_LTR : ViewCompat.LAYOUT_DIRECTION_RTL));
+        // Make sure it's scrolled all the way to the start
+        onView(withId(R.id.tabs)).perform(selectTab(0));
+
+        // Perform a series of setScrollPosition() calls
+        final AtomicInteger lastScrollX = new AtomicInteger(tabs.getScrollX());
+        for (int i = 0; i < positions.length; i++) {
+            onView(withId(R.id.tabs))
+                    .perform(setScrollPosition(positions[i], positionOffsets[i]))
+                    .check(new ViewAssertion() {
+                        @Override
+                        public void check(View view, NoMatchingViewException notFoundException) {
+                            if (view == null) {
+                                throw notFoundException;
+                            }
+                            // Verify increasing or decreasing scroll X values
+                            int sx = view.getScrollX();
+                            assertTrue(isLtr ? sx > lastScrollX.get() : sx < lastScrollX.get());
+                            lastScrollX.set(sx);
+                        }
+                    });
+        }
+
+        Espresso.unregisterIdlingResources(idler);
+    }
+
+    static class TabLayoutScrollIdlingResource implements IdlingResource {
+
+        private boolean mIsIdle = true;
+        private ResourceCallback mCallback;
+
+        TabLayoutScrollIdlingResource(final TabLayout tabLayout) {
+            tabLayout.setScrollAnimatorListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(ValueAnimatorCompat animator) {
+                    setIdle(false);
+                }
+
+                @Override
+                public void onAnimationEnd(ValueAnimatorCompat animator) {
+                    setIdle(true);
+                }
+            });
+        }
+
+        @Override
+        public String getName() {
+            return "TabLayoutScrollIdlingResource";
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            return mIsIdle;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback callback) {
+            mCallback = callback;
+        }
+
+        private void setIdle(boolean idle) {
+            boolean wasIdle = mIsIdle;
+            mIsIdle = idle;
+            if (mIsIdle && !wasIdle && mCallback != null) {
+                mCallback.onTransitionToIdle();
+            }
+        }
+    }
 }
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 3b524e1..929fcd7 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -460,6 +460,12 @@
     }
 
     @Test
+    public void testTextSetViaAttributeCollapsedHint() {
+        onView(withId(R.id.textinput_with_text))
+                .check(isHintExpanded(false));
+    }
+
+    @Test
     public void testFocusMovesToEditTextWithPasswordEnabled() {
         // Focus the preceding EditText
         onView(withId(R.id.textinput_edittext))
@@ -474,12 +480,6 @@
                 .check(matches(hasFocus()));
     }
 
-    @Test
-    public void testTextSetViaAttributeCollapsedHint() {
-        onView(withId(R.id.textinput_with_text))
-                .check(isHintExpanded(false));
-    }
-
     static ViewAssertion isHintExpanded(final boolean expanded) {
         return new ViewAssertion() {
             @Override
diff --git a/exifinterface/Android.mk b/exifinterface/Android.mk
index 9da8bc5..4d5887c 100644
--- a/exifinterface/Android.mk
+++ b/exifinterface/Android.mk
@@ -25,6 +25,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations
 LOCAL_JAR_EXCLUDE_FILES := none
diff --git a/exifinterface/AndroidManifest-make.xml b/exifinterface/AndroidManifest-make.xml
new file mode 100644
index 0000000..4812a71
--- /dev/null
+++ b/exifinterface/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.exifinterface">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/exifinterface/AndroidManifest.xml b/exifinterface/AndroidManifest.xml
index 4812a71..bacbdbe 100644
--- a/exifinterface/AndroidManifest.xml
+++ b/exifinterface/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.exifinterface">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/exifinterface/build.gradle b/exifinterface/build.gradle
index 1bb5d45..c65cf71 100644
--- a/exifinterface/build.gradle
+++ b/exifinterface/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'exifinterface'
 
 dependencies {
@@ -20,7 +19,6 @@
     sourceSets {
         main.manifest.srcFile 'AndroidManifest.xml'
         main.java.srcDirs = ['src']
-        main.res.srcDirs = ['res']
 
         androidTest.setRoot('tests')
         androidTest.java.srcDir 'tests/src'
@@ -42,28 +40,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/exifinterface/src/android/support/media/ExifInterface.java b/exifinterface/src/android/support/media/ExifInterface.java
index dd7ece0..4830ecc 100644
--- a/exifinterface/src/android/support/media/ExifInterface.java
+++ b/exifinterface/src/android/support/media/ExifInterface.java
@@ -658,9 +658,9 @@
         }
 
         private Object getValue(ByteOrder byteOrder) {
+            ByteOrderedDataInputStream inputStream = null;
             try {
-                ByteOrderedDataInputStream inputStream =
-                        new ByteOrderedDataInputStream(bytes);
+                inputStream = new ByteOrderedDataInputStream(bytes);
                 inputStream.setByteOrder(byteOrder);
                 switch (format) {
                     case IFD_FORMAT_BYTE:
@@ -768,6 +768,14 @@
             } catch (IOException e) {
                 Log.w(TAG, "IOException occurred during reading a value", e);
                 return null;
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                        Log.e(TAG, "IOException occurred while closing InputStream", e);
+                    }
+                }
             }
         }
 
@@ -1842,11 +1850,31 @@
     }
 
     /**
-     * Stores the latitude and longitude value in a float array. The first element is
-     * the latitude, and the second element is the longitude. Returns false if the
-     * Exif tags are not available.
+     * Stores the latitude and longitude value in a float array. The first element is the latitude,
+     * and the second element is the longitude. Returns false if the Exif tags are not available.
+     *
+     * @deprecated Use {@link #getLatLong()} instead.
      */
+    @Deprecated
     public boolean getLatLong(float output[]) {
+        double[] latLong = getLatLong();
+        if (latLong == null) {
+            return false;
+        }
+
+        output[0] = (float) latLong[0];
+        output[1] = (float) latLong[1];
+        return true;
+    }
+
+    /**
+     * Gets the latitude and longitude values.
+     * <p>
+     * If there are valid latitude and longitude values in the image, this method returns a double
+     * array where the first element is the latitude and the second element is the longitude.
+     * Otherwise, it returns null.
+     */
+    public double[] getLatLong() {
         String latValue = getAttribute(TAG_GPS_LATITUDE);
         String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
         String lngValue = getAttribute(TAG_GPS_LONGITUDE);
@@ -1854,16 +1882,39 @@
 
         if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
             try {
-                output[0] = convertRationalLatLonToFloat(latValue, latRef);
-                output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
-                return true;
+                double latitude = convertRationalLatLonToDouble(latValue, latRef);
+                double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
+                return new double[] {latitude, longitude};
             } catch (IllegalArgumentException e) {
                 Log.w(TAG, "Latitude/longitude values are not parseable. " +
                         String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
                                 latValue, latRef, lngValue, lngRef));
             }
         }
-        return false;
+        return null;
+    }
+
+    /**
+     * Sets the latitude and longitude values.
+     *
+     * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
+     *                 90.0.
+     * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
+     *                  and 180.0.
+     * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
+     *                                  specified range.
+     */
+    public void setLatLong(double latitude, double longitude) {
+        if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
+            throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
+        }
+        if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
+            throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
+        }
+        setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
+        setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
+        setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
+        setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
     }
 
     /**
@@ -1945,7 +1996,7 @@
         }
     }
 
-    private static float convertRationalLatLonToFloat(String rationalString, String ref) {
+    private static double convertRationalLatLonToDouble(String rationalString, String ref) {
         try {
             String [] parts = rationalString.split(",");
 
@@ -1964,15 +2015,26 @@
 
             double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
             if ((ref.equals("S") || ref.equals("W"))) {
-                return (float) -result;
+                return -result;
+            } else if (ref.equals("N") || ref.equals("E")) {
+                return result;
+            } else {
+                // Not valid
+                throw new IllegalArgumentException();
             }
-            return (float) result;
         } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
             // Not valid
             throw new IllegalArgumentException();
         }
     }
 
+    private String convertDecimalDegree(double decimalDegree) {
+        long degrees = (long) decimalDegree;
+        long minutes = (long) ((decimalDegree - degrees) * 60.0);
+        long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
+        return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
+    }
+
     // Checks the type of image file
     private int getMimeType(BufferedInputStream in) throws IOException {
         in.mark(SIGNATURE_CHECK_SIZE);
@@ -2040,6 +2102,7 @@
         signatureInputStream.setByteOrder(mExifByteOrder);
 
         short orfSignature = signatureInputStream.readShort();
+        signatureInputStream.close();
         return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
     }
 
@@ -2056,6 +2119,7 @@
         signatureInputStream.setByteOrder(mExifByteOrder);
 
         short signatureByte = signatureInputStream.readShort();
+        signatureInputStream.close();
         return signatureByte == RW2_SIGNATURE;
     }
 
diff --git a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
index 87fb950..ee9e38e 100644
--- a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
+++ b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
@@ -16,6 +16,13 @@
 
 package android.support.media;
 
+import static android.support.test.InstrumentationRegistry.getContext;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.os.Environment;
@@ -40,12 +47,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 
-import static android.support.test.InstrumentationRegistry.getContext;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.fail;
-
 /**
  * Test {@link ExifInterface}.
  */
@@ -53,7 +54,6 @@
 public class ExifInterfaceTest {
     private static final String TAG = ExifInterface.class.getSimpleName();
     private static final boolean VERBOSE = false;  // lots of logging
-
     private static final double DIFFERENCE_TOLERANCE = .001;
 
     private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
@@ -64,6 +64,20 @@
     private static final String[] IMAGE_FILENAMES = new String[] {
             EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG};
 
+    private static final String TEST_TEMP_FILE_NAME = "testImage";
+    private static final double DELTA = 1e-8;
+    private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8;
+    private static final double[] TEST_LATITUDE_VALID_VALUES = new double[]
+            {0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737};
+    private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[]
+            {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352};
+    private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[]
+            {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001,
+                    263.34763236326, -1e5, 347.32525, -176.346347754};
+    private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[]
+            {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001,
+                    263.34763236326, -1e10, 347.325252623, -4000.346323236};
+
     private static final String[] EXIF_TAGS = {
             ExifInterface.TAG_MAKE,
             ExifInterface.TAG_MODEL,
@@ -235,6 +249,52 @@
         }
     }
 
+    @Test
+    @SmallTest
+    public void testSetLatLong_withValidValues() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+
+            double[] latLong = exif.getLatLong();
+            assertNotNull(latLong);
+            assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA);
+            assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSetLatLong_withInvalidLatitude() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            try {
+                exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            assertNull(exif.getLatLong());
+            assertLatLongValuesAreNotSet(exif);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSetLatLong_withInvalidLongitude() throws IOException {
+        for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+            ExifInterface exif = createTestExifInterface();
+            try {
+                exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            assertNull(exif.getLatLong());
+            assertLatLongValuesAreNotSet(exif);
+        }
+    }
+
     private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
         // Prints thumbnail information.
         if (exifInterface.hasThumbnail()) {
@@ -264,8 +324,8 @@
         // Prints GPS information.
         Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
 
-        float[] latLong = new float[2];
-        if (exifInterface.getLatLong(latLong)) {
+        double[] latLong = exifInterface.getLatLong();
+        if (latLong != null) {
             Log.v(TAG, fileName + " Latitude = " + latLong[0]);
             Log.v(TAG, fileName + " Longitude = " + latLong[1]);
         } else {
@@ -318,8 +378,8 @@
         }
 
         // Checks GPS information.
-        float[] latLong = new float[2];
-        assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+        double[] latLong = exifInterface.getLatLong();
+        assertEquals(expectedValue.hasLatLong, latLong != null);
         if (expectedValue.hasLatLong) {
             assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
             assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
@@ -443,4 +503,17 @@
         }
         return total;
     }
+
+    private void assertLatLongValuesAreNotSet(ExifInterface exif) {
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF));
+    }
+
+    private ExifInterface createTestExifInterface() throws IOException {
+        File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg");
+        image.deleteOnExit();
+        return new ExifInterface(image.getAbsolutePath());
+    }
 }
diff --git a/fragment/Android.mk b/fragment/Android.mk
index a41b0c2..f55cfb7 100644
--- a/fragment/Android.mk
+++ b/fragment/Android.mk
@@ -36,6 +36,7 @@
     $(call all-java-files-under, api21) \
     $(call all-java-files-under, java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-core-ui \
diff --git a/fragment/AndroidManifest-make.xml b/fragment/AndroidManifest-make.xml
new file mode 100644
index 0000000..54e61d3
--- /dev/null
+++ b/fragment/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.fragment">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.fragment"/>
+    <application />
+</manifest>
diff --git a/fragment/AndroidManifest.xml b/fragment/AndroidManifest.xml
index 54e61d3..9b34e14 100644
--- a/fragment/AndroidManifest.xml
+++ b/fragment/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.fragment">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.fragment"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/fragment/build.gradle b/fragment/build.gradle
index 26e9f1c..7c9098a 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -18,9 +18,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -50,11 +47,6 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -65,22 +57,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -88,7 +64,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/fragment/java/android/support/v4/app/BackStackRecord.java b/fragment/java/android/support/v4/app/BackStackRecord.java
index 3b41d60..923c366 100644
--- a/fragment/java/android/support/v4/app/BackStackRecord.java
+++ b/fragment/java/android/support/v4/app/BackStackRecord.java
@@ -639,6 +639,7 @@
             LogWriter logw = new LogWriter(TAG);
             PrintWriter pw = new PrintWriter(logw);
             dump("  ", null, pw, null);
+            pw.close();
         }
         mCommitted = true;
         if (mAddToBackStack) {
@@ -761,8 +762,11 @@
     /**
      * Reverses the execution of the operations within this transaction. The Fragment states will
      * only be modified if optimizations are not allowed.
+     *
+     * @param moveToState {@code true} if added fragments should be moved to their final state
+     *                    in unoptimized transactions
      */
-    void executePopOps() {
+    void executePopOps(boolean moveToState) {
         for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
             final Op op = mOps.get(opNum);
             Fragment f = op.fragment;
@@ -799,7 +803,7 @@
                 mManager.moveFragmentToExpectedState(f);
             }
         }
-        if (!mAllowOptimization) {
+        if (!mAllowOptimization && moveToState) {
             mManager.moveToState(mManager.mCurState, true);
         }
     }
diff --git a/fragment/java/android/support/v4/app/Fragment.java b/fragment/java/android/support/v4/app/Fragment.java
index 881c2b4..06d0b1e 100644
--- a/fragment/java/android/support/v4/app/Fragment.java
+++ b/fragment/java/android/support/v4/app/Fragment.java
@@ -2150,6 +2150,9 @@
     }
 
     void instantiateChildFragmentManager() {
+        if (mHost == null) {
+            throw new IllegalStateException("Fragment has not been attached yet.");
+        }
         mChildFragmentManager = new FragmentManagerImpl();
         mChildFragmentManager.attachController(mHost, new FragmentContainer() {
             @Override
diff --git a/fragment/java/android/support/v4/app/FragmentActivity.java b/fragment/java/android/support/v4/app/FragmentActivity.java
index 0b38fd4..78e5370 100644
--- a/fragment/java/android/support/v4/app/FragmentActivity.java
+++ b/fragment/java/android/support/v4/app/FragmentActivity.java
@@ -113,8 +113,8 @@
 
     boolean mCreated;
     boolean mResumed;
-    boolean mStopped;
-    boolean mReallyStopped;
+    boolean mStopped = true;
+    boolean mReallyStopped = true;
     boolean mRetaining;
 
     boolean mOptionsMenuInvalidated;
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index 8aaf53f..58c0dd0 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -1968,7 +1968,12 @@
             mTmpRecords = new ArrayList<>();
             mTmpIsPop = new ArrayList<>();
         }
-        executePostponedTransaction(null, null);
+        mExecutingActions = true;
+        try {
+            executePostponedTransaction(null, null);
+        } finally {
+            mExecutingActions = false;
+        }
     }
 
     public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
@@ -2133,8 +2138,6 @@
             } else {
                 record.trackAddedFragmentsInPop(mTmpAddedFragments);
             }
-            final int bumpAmount = isPop ? -1 : 1;
-            record.bumpBackStackNesting(bumpAmount);
             addToBackStack = addToBackStack || record.mAddToBackStack;
         }
         mTmpAddedFragments.clear();
@@ -2232,7 +2235,7 @@
                 if (isPop) {
                     record.executeOps();
                 } else {
-                    record.executePopOps();
+                    record.executePopOps(false);
                 }
 
                 // move to the end
@@ -2350,8 +2353,13 @@
             final BackStackRecord record = records.get(i);
             final boolean isPop = isRecordPop.get(i);
             if (isPop) {
-                record.executePopOps();
+                record.bumpBackStackNesting(-1);
+                // Only execute the add operations at the end of
+                // all transactions.
+                boolean moveToState = i == (endIndex - 1);
+                record.executePopOps(moveToState);
             } else {
+                record.bumpBackStackNesting(1);
                 record.executeOps();
             }
         }
@@ -2855,6 +2863,7 @@
                     LogWriter logw = new LogWriter(TAG);
                     PrintWriter pw = new PrintWriter(logw);
                     bse.dump("  ", pw, false);
+                    pw.close();
                 }
                 mBackStack.add(bse);
                 if (bse.mIndex >= 0) {
@@ -2880,26 +2889,36 @@
 
     public void dispatchCreate() {
         mStateSaved = false;
+        mExecutingActions = true;
         moveToState(Fragment.CREATED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchActivityCreated() {
         mStateSaved = false;
+        mExecutingActions = true;
         moveToState(Fragment.ACTIVITY_CREATED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchStart() {
         mStateSaved = false;
+        mExecutingActions = true;
         moveToState(Fragment.STARTED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchResume() {
         mStateSaved = false;
+        mExecutingActions = true;
         moveToState(Fragment.RESUMED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchPause() {
+        mExecutingActions = true;
         moveToState(Fragment.STARTED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchStop() {
@@ -2908,21 +2927,29 @@
         // them.
         mStateSaved = true;
 
+        mExecutingActions = true;
         moveToState(Fragment.STOPPED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchReallyStop() {
+        mExecutingActions = true;
         moveToState(Fragment.ACTIVITY_CREATED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchDestroyView() {
+        mExecutingActions = true;
         moveToState(Fragment.CREATED, false);
+        mExecutingActions = false;
     }
 
     public void dispatchDestroy() {
         mDestroyed = true;
         execPendingActions();
+        mExecutingActions = true;
         moveToState(Fragment.INITIALIZING, false);
+        mExecutingActions = false;
         mHost = null;
         mContainer = null;
         mParent = null;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
index 33e20d0..a822a0c 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
@@ -359,7 +359,7 @@
     @Test
     public void saveWhileAnimatingAway() throws Throwable {
         final FragmentController fc1 = FragmentTestUtil.createController(mActivityRule);
-        FragmentTestUtil.resume(fc1, null);
+        FragmentTestUtil.resume(mActivityRule, fc1, null);
 
         final FragmentManager fm1 = fc1.getSupportFragmentManager();
 
@@ -399,10 +399,10 @@
         assertEquals(fragment2, fm1.findFragmentByTag("2")); // still exists because it is animating
 
         Pair<Parcelable, FragmentManagerNonConfig> state =
-                FragmentTestUtil.destroy(fc1);
+                FragmentTestUtil.destroy(mActivityRule, fc1);
 
         final FragmentController fc2 = FragmentTestUtil.createController(mActivityRule);
-        FragmentTestUtil.resume(fc2, state);
+        FragmentTestUtil.resume(mActivityRule, fc2, state);
 
         final FragmentManager fm2 = fc2.getSupportFragmentManager();
         Fragment fragment2restored = fm2.findFragmentByTag("2");
diff --git a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
index 4c0c0a4..c9d7351a 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
@@ -26,24 +26,30 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks;
 import android.support.v4.app.test.EmptyFragmentTestActivity;
+import android.support.v4.app.test.FragmentTestActivity;
 import android.support.v4.view.ViewCompat;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -57,6 +63,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -550,6 +557,81 @@
         assertTrue(fragmentA.mCalledOnDestroy);
     }
 
+    // Make sure that executing transactions during activity lifecycle events
+    // is properly prevented.
+    @Test
+    public void preventReentrantCalls() throws Throwable {
+        testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.CREATED);
+        testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ACTIVITY_CREATED);
+        testLifecycleTransitionFailure(StrictFragment.ACTIVITY_CREATED, StrictFragment.STARTED);
+        testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.RESUMED);
+
+        testLifecycleTransitionFailure(StrictFragment.RESUMED, StrictFragment.STARTED);
+        testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.CREATED);
+        testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ATTACHED);
+        testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.DETACHED);
+    }
+
+    private void testLifecycleTransitionFailure(final int fromState,
+            final int toState) throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final FragmentController fc1 = FragmentController.createController(
+                        new HostCallbacks(mActivityRule.getActivity()));
+                FragmentTestUtil.resume(mActivityRule, fc1, null);
+
+                final FragmentManager fm1 = fc1.getSupportFragmentManager();
+
+                final Fragment reentrantFragment = ReentrantFragment.create(fromState, toState);
+
+                fm1.beginTransaction()
+                        .add(reentrantFragment, "reentrant")
+                        .commit();
+                try {
+                    fm1.executePendingTransactions();
+                } catch (IllegalStateException e) {
+                    fail("An exception shouldn't happen when initially adding the fragment");
+                }
+
+                // Now shut down the fragment controller. When fromState > toState, this should
+                // result in an exception
+                Pair<Parcelable, FragmentManagerNonConfig> savedState = null;
+                try {
+                    savedState = FragmentTestUtil.destroy(mActivityRule, fc1);
+                    if (fromState > toState) {
+                        fail("Expected IllegalStateException when moving from "
+                                + StrictFragment.stateToString(fromState) + " to "
+                                + StrictFragment.stateToString(toState));
+                    }
+                } catch (IllegalStateException e) {
+                    if (fromState < toState) {
+                        fail("Unexpected IllegalStateException when moving from "
+                                + StrictFragment.stateToString(fromState) + " to "
+                                + StrictFragment.stateToString(toState));
+                    }
+                    return; // test passed!
+                }
+
+                // now restore from saved state. This will be reached when
+                // fromState < toState. We want to catch the fragment while it
+                // is being restored as the fragment controller state is being brought up.
+
+                final FragmentController fc2 = FragmentController.createController(
+                        new HostCallbacks(mActivityRule.getActivity()));
+                try {
+                    FragmentTestUtil.resume(mActivityRule, fc2, savedState);
+
+                    fail("Expected IllegalStateException when moving from "
+                            + StrictFragment.stateToString(fromState) + " to "
+                            + StrictFragment.stateToString(toState));
+                } catch (IllegalStateException e) {
+                    // expected, so the test passed!
+                }
+            }
+        });
+    }
+
     /**
      * Test to ensure that when dispatch* is called that the fragment manager
      * doesn't cause the contained fragment states to change even if no state changes.
@@ -586,6 +668,22 @@
         assertFalse(fragment1.mCalledOnResume);
     }
 
+    /**
+     * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @Test
+    public void fragmentActivityFinishEarly() throws Throwable {
+        Intent intent = new Intent(mActivityRule.getActivity(), FragmentTestActivity.class);
+        intent.putExtra("finishEarly", true);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        FragmentTestActivity activity = (FragmentTestActivity)
+                InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
+
+        assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
+    }
+
     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/FragmentOptimizationTest.java b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
index 06abd69..13901c4 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
@@ -15,11 +15,14 @@
  */
 package android.support.v4.app;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
 import android.support.fragment.test.R;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
@@ -648,38 +651,30 @@
 
     // Test that a fragment view that is created with focus has focus after the transaction
     // completes.
+    @UiThreadTest
     @Test
     public void focusedView() throws Throwable {
-        FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
+        mActivityRule.getActivity().setContentView(R.layout.double_container);
         mContainer = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
-        final EditText firstEditText = new EditText(mContainer.getContext());
-        mInstrumentation.runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mContainer.addView(firstEditText);
-                firstEditText.requestFocus();
-            }
-        });
+        EditText firstEditText = new EditText(mContainer.getContext());
+        mContainer.addView(firstEditText);
+        firstEditText.requestFocus();
+
         assertTrue(firstEditText.isFocused());
         final CountCallsFragment fragment1 = new CountCallsFragment();
         final CountCallsFragment fragment2 = new CountCallsFragment();
         fragment2.setLayoutId(R.layout.with_edit_text);
-        mInstrumentation.runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mFM.beginTransaction()
-                        .add(R.id.fragmentContainer2, fragment1)
-                        .addToBackStack(null)
-                        .setAllowOptimization(true)
-                        .commit();
-                mFM.beginTransaction()
-                        .replace(R.id.fragmentContainer2, fragment2)
-                        .addToBackStack(null)
-                        .setAllowOptimization(true)
-                        .commit();
-                mFM.executePendingTransactions();
-            }
-        });
+        mFM.beginTransaction()
+                .add(R.id.fragmentContainer2, fragment1)
+                .addToBackStack(null)
+                .setAllowOptimization(true)
+                .commit();
+        mFM.beginTransaction()
+                .replace(R.id.fragmentContainer2, fragment2)
+                .addToBackStack(null)
+                .setAllowOptimization(true)
+                .commit();
+        mFM.executePendingTransactions();
         final View editText = fragment2.getView().findViewById(R.id.editText);
         assertTrue(editText.isFocused());
         assertFalse(firstEditText.isFocused());
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index ba5875a..a37b82e 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -17,8 +17,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.app.Activity;
 import android.app.Instrumentation;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
@@ -35,14 +37,30 @@
         }
     };
 
-    public static void waitForExecution(final ActivityTestRule<FragmentTestActivity> rule) {
+    public static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) {
         // Wait for two cycles. When starting a postponed transition, it will post to
         // the UI thread and then the execution will be added onto the queue after that.
         // The two-cycle wait makes sure fragments have the opportunity to complete both
         // before returning.
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(DO_NOTHING);
-        instrumentation.runOnMainSync(DO_NOTHING);
+        try {
+            rule.runOnUiThread(DO_NOTHING);
+            rule.runOnUiThread(DO_NOTHING);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
+    }
+
+    private static void runOnUiThreadRethrow(ActivityTestRule<? extends Activity> rule,
+            Runnable r) {
+        if (Looper.getMainLooper() == Looper.myLooper()) {
+            r.run();
+        } else {
+            try {
+                rule.runOnUiThread(r);
+            } catch (Throwable t) {
+                throw new RuntimeException(t);
+            }
+        }
     }
 
     public static boolean executePendingTransactions(
@@ -123,8 +141,7 @@
     public static FragmentController createController(ActivityTestRule<FragmentTestActivity> rule) {
         final FragmentController[] controller = new FragmentController[1];
         final FragmentTestActivity activity = rule.getActivity();
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(new Runnable() {
+        runOnUiThreadRethrow(rule, new Runnable() {
             @Override
             public void run() {
                 Handler handler = new Handler();
@@ -135,10 +152,10 @@
         return controller[0];
     }
 
-    public static void resume(final FragmentController fragmentController,
+    public static void resume(ActivityTestRule<? extends Activity> rule,
+            final FragmentController fragmentController,
             final Pair<Parcelable, FragmentManagerNonConfig> savedState) {
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(new Runnable() {
+        runOnUiThreadRethrow(rule, new Runnable() {
             @Override
             public void run() {
                 fragmentController.attachHost(null);
@@ -158,10 +175,10 @@
     }
 
     public static Pair<Parcelable, FragmentManagerNonConfig> destroy(
+            ActivityTestRule<? extends Activity> rule,
             final FragmentController fragmentController) {
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         final Pair<Parcelable, FragmentManagerNonConfig>[] result = new Pair[1];
-        instrumentation.runOnMainSync(new Runnable() {
+        runOnUiThreadRethrow(rule, new Runnable() {
             @Override
             public void run() {
                 fragmentController.dispatchPause();
diff --git a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
index 521eb84..ace24e9 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
@@ -26,6 +26,7 @@
 
 import android.app.Instrumentation;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
@@ -1002,6 +1003,56 @@
         FragmentTestUtil.assertChildren(innerContainer, fragment2);
     }
 
+    // Popping the backstack with non-optimized fragments should execute the operations together.
+    // When a non-backstack fragment will be raised, it should not be destroyed.
+    @Test
+    public void popToNonBackStackFragment() throws Throwable {
+        FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
+        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
+
+        final SimpleViewFragment fragment1 = new SimpleViewFragment();
+
+        fm.beginTransaction()
+                .add(R.id.fragmentContainer, fragment1)
+                .commit();
+
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+
+        final SimpleViewFragment fragment2 = new SimpleViewFragment();
+
+        fm.beginTransaction()
+                .replace(R.id.fragmentContainer, fragment2)
+                .addToBackStack("two")
+                .commit();
+
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+
+        final SimpleViewFragment fragment3 = new SimpleViewFragment();
+
+        fm.beginTransaction()
+                .replace(R.id.fragmentContainer, fragment3)
+                .addToBackStack("three")
+                .commit();
+
+        FragmentTestUtil.executePendingTransactions(mActivityRule);
+
+        assertEquals(1, fragment1.onCreateViewCount);
+        assertEquals(1, fragment2.onCreateViewCount);
+        assertEquals(1, fragment3.onCreateViewCount);
+
+        FragmentTestUtil.popBackStackImmediate(mActivityRule, "two",
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+
+        ViewGroup container = (ViewGroup)
+                mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
+
+        FragmentTestUtil.assertChildren(container, fragment1);
+
+        assertEquals(2, fragment1.onCreateViewCount);
+        assertEquals(1, fragment2.onCreateViewCount);
+        assertEquals(1, fragment3.onCreateViewCount);
+    }
+
     private View findViewById(int viewId) {
         return mActivityRule.getActivity().findViewById(viewId);
     }
@@ -1046,4 +1097,16 @@
             return view;
         }
     }
+
+    public static class SimpleViewFragment extends Fragment {
+        public int onCreateViewCount;
+
+        @Nullable
+        @Override
+        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+                @Nullable Bundle savedInstanceState) {
+            onCreateViewCount++;
+            return inflater.inflate(R.layout.fragment_a, container, false);
+        }
+    }
 }
diff --git a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
index 0f07b89..dbe0422 100644
--- a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
+++ b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.Instrumentation;
 import android.os.Build;
@@ -675,7 +676,7 @@
     @Test
     public void saveWhilePostponed() throws Throwable {
         final FragmentController fc1 = FragmentTestUtil.createController(mActivityRule);
-        FragmentTestUtil.resume(fc1, null);
+        FragmentTestUtil.resume(mActivityRule, fc1, null);
 
         final FragmentManager fm1 = fc1.getSupportFragmentManager();
 
@@ -688,10 +689,10 @@
         FragmentTestUtil.waitForExecution(mActivityRule);
 
         Pair<Parcelable, FragmentManagerNonConfig> state =
-                FragmentTestUtil.destroy(fc1);
+                FragmentTestUtil.destroy(mActivityRule, fc1);
 
         final FragmentController fc2 = FragmentTestUtil.createController(mActivityRule);
-        FragmentTestUtil.resume(fc2, state);
+        FragmentTestUtil.resume(mActivityRule, fc2, state);
 
         final FragmentManager fm2 = fc2.getSupportFragmentManager();
         Fragment fragment2 = fm2.findFragmentByTag("1");
@@ -718,6 +719,43 @@
         assertNull(fragment2.getView());
     }
 
+    // Ensure that the postponed fragment transactions don't allow reentrancy in fragment manager
+    @Test
+    public void postponeDoesNotAllowReentrancy() throws Throwable {
+        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
+        final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
+
+        final CommitNowFragment fragment = new CommitNowFragment();
+        fm.beginTransaction()
+                .addSharedElement(startBlue, "blueSquare")
+                .replace(R.id.fragmentContainer, fragment)
+                .setAllowOptimization(true)
+                .addToBackStack(null)
+                .commit();
+
+        FragmentTestUtil.waitForExecution(mActivityRule);
+
+        // should be postponed now
+        assertPostponedTransition(mBeginningFragment, fragment, null);
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // start the postponed transition
+                fragment.startPostponedEnterTransition();
+
+                try {
+                    // This should trigger an IllegalStateException
+                    fm.executePendingTransactions();
+                    fail("commitNow() while executing a transaction should cause an "
+                            + "IllegalStateException");
+                } catch (IllegalStateException e) {
+                    // expected
+                }
+            }
+        });
+    }
+
     private void assertPostponedTransition(TransitionFragment fromFragment,
             TransitionFragment toFragment, TransitionFragment removedFragment)
             throws InterruptedException {
@@ -856,4 +894,15 @@
             return inflater.inflate(R.layout.scene2, container, false);
         }
     }
+
+    public static class CommitNowFragment extends PostponedFragment1 {
+        @Override
+        public void onResume() {
+            super.onResume();
+            // This should throw because this happens during the execution
+            getFragmentManager().beginTransaction()
+                    .add(R.id.fragmentContainer, new PostponedFragment1())
+                    .commitNow();
+        }
+    }
 }
diff --git a/fragment/tests/java/android/support/v4/app/ReentrantFragment.java b/fragment/tests/java/android/support/v4/app/ReentrantFragment.java
new file mode 100644
index 0000000..472245d
--- /dev/null
+++ b/fragment/tests/java/android/support/v4/app/ReentrantFragment.java
@@ -0,0 +1,69 @@
+/*
+ * 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.app;
+
+import android.os.Bundle;
+
+public class ReentrantFragment extends StrictFragment {
+    private static final String FROM_STATE = "fromState";
+    private static final String TO_STATE = "toState";
+    int mFromState = 0;
+    int mToState = 0;
+    boolean mIsRestored;
+
+    public static ReentrantFragment create(int fromState, int toState) {
+        ReentrantFragment fragment = new ReentrantFragment();
+        fragment.mFromState = fromState;
+        fragment.mToState = toState;
+        fragment.mIsRestored = false;
+        return fragment;
+    }
+
+    @Override
+    public void onStateChanged(int fromState) {
+        super.onStateChanged(fromState);
+        // We execute the transaction when shutting down or after restoring
+        if (fromState == mFromState && mState == mToState
+                && (mToState < mFromState || mIsRestored)) {
+            executeTransaction();
+        }
+    }
+
+    private void executeTransaction() {
+        getFragmentManager().beginTransaction()
+                .add(new StrictFragment(), "should throw")
+                .commitNow();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(FROM_STATE, mFromState);
+        outState.putInt(TO_STATE, mToState);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        if (savedInstanceState != null) {
+            mFromState = savedInstanceState.getInt(FROM_STATE);
+            mToState = savedInstanceState.getInt(TO_STATE);
+            mIsRestored = true;
+        }
+        super.onCreate(savedInstanceState);
+    }
+}
+
diff --git a/fragment/tests/java/android/support/v4/app/StrictFragment.java b/fragment/tests/java/android/support/v4/app/StrictFragment.java
index 7e57745..033eb65 100644
--- a/fragment/tests/java/android/support/v4/app/StrictFragment.java
+++ b/fragment/tests/java/android/support/v4/app/StrictFragment.java
@@ -51,6 +51,10 @@
         return "(unknown " + state + ")";
     }
 
+    public void onStateChanged(int fromState) {
+        checkGetActivity();
+    }
+
     public void checkGetActivity() {
         if (getActivity() == null) {
             throw new IllegalStateException("getActivity() returned null at unexpected time");
@@ -92,7 +96,7 @@
         mCalledOnAttach = true;
         checkState("onAttach", DETACHED);
         mState = ATTACHED;
-        checkGetActivity();
+        onStateChanged(DETACHED);
     }
 
     @Override
@@ -104,7 +108,7 @@
         mCalledOnCreate = true;
         checkState("onCreate", ATTACHED);
         mState = CREATED;
-        checkGetActivity();
+        onStateChanged(ATTACHED);
     }
 
     @Override
@@ -112,8 +116,9 @@
         super.onActivityCreated(savedInstanceState);
         mCalledOnActivityCreated = true;
         checkState("onActivityCreated", ATTACHED, CREATED);
+        int fromState = mState;
         mState = ACTIVITY_CREATED;
-        checkGetActivity();
+        onStateChanged(fromState);
     }
 
     @Override
@@ -122,7 +127,7 @@
         mCalledOnStart = true;
         checkState("onStart", ACTIVITY_CREATED);
         mState = STARTED;
-        checkGetActivity();
+        onStateChanged(ACTIVITY_CREATED);
     }
 
     @Override
@@ -131,7 +136,7 @@
         mCalledOnResume = true;
         checkState("onResume", STARTED);
         mState = RESUMED;
-        checkGetActivity();
+        onStateChanged(STARTED);
     }
 
     @Override
@@ -151,7 +156,7 @@
         mCalledOnPause = true;
         checkState("onPause", RESUMED);
         mState = STARTED;
-        checkGetActivity();
+        onStateChanged(RESUMED);
     }
 
     @Override
@@ -160,7 +165,7 @@
         mCalledOnStop = true;
         checkState("onStop", STARTED);
         mState = CREATED;
-        checkGetActivity();
+        onStateChanged(STARTED);
     }
 
     @Override
@@ -169,7 +174,7 @@
         mCalledOnDestroy = true;
         checkState("onDestroy", CREATED);
         mState = ATTACHED;
-        checkGetActivity();
+        onStateChanged(CREATED);
     }
 
     @Override
@@ -177,7 +182,8 @@
         super.onDetach();
         mCalledOnDetach = true;
         checkState("onDestroy", CREATED, ATTACHED);
+        int fromState = mState;
         mState = DETACHED;
-        checkGetActivity();
+        onStateChanged(fromState);
     }
 }
diff --git a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
index 5d13fe7..ef8cd0a 100644
--- a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
@@ -15,12 +15,15 @@
  */
 package android.support.v4.app.test;
 
+import static org.junit.Assert.assertFalse;
+
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
@@ -38,10 +41,25 @@
  * A simple activity used for Fragment Transitions and lifecycle event ordering
  */
 public class FragmentTestActivity extends FragmentActivity {
+    public final CountDownLatch onDestroyLatch = new CountDownLatch(1);
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
         setContentView(R.layout.activity_content);
+        Intent intent = getIntent();
+        if (intent != null && intent.getBooleanExtra("finishEarly", false)) {
+            finish();
+            getSupportFragmentManager().beginTransaction()
+                    .add(new AssertNotDestroyed(), "not destroyed")
+                    .commit();
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        onDestroyLatch.countDown();
     }
 
     public static class TestFragment extends Fragment {
@@ -258,4 +276,14 @@
             onActivityResultResultCode = resultCode;
         }
     }
+
+    public static class AssertNotDestroyed extends Fragment {
+        @Override
+        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+            super.onActivityCreated(savedInstanceState);
+            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+                assertFalse(getActivity().isDestroyed());
+            }
+        }
+    }
 }
diff --git a/gradle.properties b/gradle.properties
index 34b3995..fb870a9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,4 @@
 org.gradle.jvmargs=-Xmx3g
 org.gradle.daemon=true
-org.gradle.configureondemand=true
\ No newline at end of file
+org.gradle.configureondemand=true
+org.gradle.parallel=true
diff --git a/graphics/drawable/Android.mk b/graphics/drawable/Android.mk
index f58493b7e..78652aa 100644
--- a/graphics/drawable/Android.mk
+++ b/graphics/drawable/Android.mk
@@ -25,7 +25,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, static/src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/static/res
-LOCAL_MANIFEST_FILE := static/AndroidManifest.xml
+LOCAL_MANIFEST_FILE := static/AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-annotations
@@ -44,7 +44,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, animated/src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/animated/res
-LOCAL_MANIFEST_FILE := animated/AndroidManifest.xml
+LOCAL_MANIFEST_FILE := animated/AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-vectordrawable \
diff --git a/graphics/drawable/animated/AndroidManifest-make.xml b/graphics/drawable/animated/AndroidManifest-make.xml
new file mode 100644
index 0000000..98f9e17
--- /dev/null
+++ b/graphics/drawable/animated/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Copyright (C) 2015 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.graphics.drawable.animated">
+    <application/>
+</manifest>
diff --git a/graphics/drawable/animated/AndroidManifest.xml b/graphics/drawable/animated/AndroidManifest.xml
index 98f9e17..58017dc 100644
--- a/graphics/drawable/animated/AndroidManifest.xml
+++ b/graphics/drawable/animated/AndroidManifest.xml
@@ -16,5 +16,6 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.graphics.drawable.animated">
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application/>
 </manifest>
diff --git a/graphics/drawable/animated/build.gradle b/graphics/drawable/animated/build.gradle
index bb110a1..10d112a 100644
--- a/graphics/drawable/animated/build.gradle
+++ b/graphics/drawable/animated/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'animated-vector-drawable'
 
 dependencies {
@@ -41,14 +40,6 @@
         additionalParameters "--no-version-vectors"
     }
 
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
-
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
@@ -62,28 +53,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java b/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
index c9ef5d1..33ff2c5 100644
--- a/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
+++ b/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
@@ -466,6 +466,7 @@
                             Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id);
                             setupAnimatorsForTarget(target, objectAnimator);
                         } else {
+                            a.recycle();
                             throw new IllegalStateException("Context can't be null when inflating" +
                                     " animators");
                         }
diff --git a/graphics/drawable/static/AndroidManifest-make.xml b/graphics/drawable/static/AndroidManifest-make.xml
new file mode 100644
index 0000000..8674cb4
--- /dev/null
+++ b/graphics/drawable/static/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Copyright (C) 2015 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.support.graphics.drawable">
+    <application/>
+</manifest>
diff --git a/graphics/drawable/static/AndroidManifest.xml b/graphics/drawable/static/AndroidManifest.xml
index e91290d..0383e4c 100644
--- a/graphics/drawable/static/AndroidManifest.xml
+++ b/graphics/drawable/static/AndroidManifest.xml
@@ -14,6 +14,8 @@
    See the License for the specific language governing permissions and
    limitations under the License.
   -->
-<manifest package="android.support.graphics.drawable">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.support.graphics.drawable">
     <application/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
 </manifest>
diff --git a/graphics/drawable/static/build.gradle b/graphics/drawable/static/build.gradle
index 2c72f9f..fdb306c 100644
--- a/graphics/drawable/static/build.gradle
+++ b/graphics/drawable/static/build.gradle
@@ -42,14 +42,6 @@
     aaptOptions {
         additionalParameters "--no-version-vectors"
     }
-
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -60,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/media-compat/Android.mk b/media-compat/Android.mk
index 52dc3d8..0050ea4 100644
--- a/media-compat/Android.mk
+++ b/media-compat/Android.mk
@@ -38,6 +38,7 @@
     $(call all-java-files-under,java) \
     $(call all-Iaidl-files-under,java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-annotations
diff --git a/media-compat/AndroidManifest-make.xml b/media-compat/AndroidManifest-make.xml
new file mode 100644
index 0000000..c971549
--- /dev/null
+++ b/media-compat/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.mediacompat">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.mediacompat"/>
+    <application />
+</manifest>
diff --git a/media-compat/AndroidManifest.xml b/media-compat/AndroidManifest.xml
index c971549..5f7b051 100644
--- a/media-compat/AndroidManifest.xml
+++ b/media-compat/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.mediacompat">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.mediacompat"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/media-compat/api23/android/support/v4/media/MediaBrowserCompatApi23.java b/media-compat/api23/android/support/v4/media/MediaBrowserCompatApi23.java
index 308c490..dfab20b 100644
--- a/media-compat/api23/android/support/v4/media/MediaBrowserCompatApi23.java
+++ b/media-compat/api23/android/support/v4/media/MediaBrowserCompatApi23.java
@@ -48,9 +48,13 @@
 
         @Override
         public void onItemLoaded(MediaBrowser.MediaItem item) {
-            Parcel parcel = Parcel.obtain();
-            item.writeToParcel(parcel, 0);
-            mItemCallback.onItemLoaded(parcel);
+            if (item == null) {
+                mItemCallback.onItemLoaded(null);
+            } else {
+                Parcel parcel = Parcel.obtain();
+                item.writeToParcel(parcel, 0);
+                mItemCallback.onItemLoaded(parcel);
+            }
         }
 
         @Override
diff --git a/media-compat/build.gradle b/media-compat/build.gradle
index 9bec2a3..8dd44bf 100644
--- a/media-compat/build.gradle
+++ b/media-compat/build.gradle
@@ -15,9 +15,6 @@
     androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -61,22 +58,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -84,7 +65,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index be0959d..414662a 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -22,6 +22,7 @@
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
 import static android.support.v4.media.MediaBrowserProtocol
         .CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
@@ -33,6 +34,8 @@
 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
@@ -121,7 +124,9 @@
      */
     public MediaBrowserCompat(Context context, ComponentName serviceComponent,
             ConnectionCallback callback, Bundle rootHints) {
-        if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+        // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
+        // and 25 devices, use the support library version of implementation on those devices.
+        if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
             mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints);
         } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
@@ -323,7 +328,30 @@
     }
 
     /**
-     * A class with information on a single media item for use in browsing media.
+     * Searches {@link MediaItem media items} from the connected service. Not all services may
+     * support this, and {@link SearchCallback#onError} will be called if not implemented.
+     *
+     * @param query The search query that contains keywords separated by space. Should not be an
+     *            empty string.
+     * @param extras The bundle of service-specific arguments to send to the media browser service.
+     *            The contents of this bundle may affect the search result.
+     * @param callback The callback to receive the search result. Must be non-null.
+     */
+    public void search(@NonNull final String query, final Bundle extras,
+            @NonNull SearchCallback callback) {
+        if (TextUtils.isEmpty(query)) {
+            throw new IllegalArgumentException("query cannot be empty");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback cannot be null");
+        }
+        mImpl.search(query, extras, callback);
+    }
+
+    /**
+     * A class with information on a single media item for use in browsing/searching media.
+     * MediaItems are application dependent so we cannot guarantee that they contain the
+     * right values.
      */
     public static class MediaItem implements Parcelable {
         private final int mFlags;
@@ -484,7 +512,7 @@
          * Returns the media id in the {@link MediaDescriptionCompat} for this item.
          * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
          */
-        public @NonNull String getMediaId() {
+        public @Nullable String getMediaId() {
             return mDescription.getMediaId();
         }
     }
@@ -573,7 +601,7 @@
         WeakReference<Subscription> mSubscriptionRef;
 
         public SubscriptionCallback() {
-            if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+            if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
                 mSubscriptionCallbackObj =
                         MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24());
                 mToken = null;
@@ -591,21 +619,21 @@
          * Called when the list of children is loaded or updated.
          *
          * @param parentId The media id of the parent media item.
-         * @param children The children which were loaded, or null if the id is invalid.
+         * @param children The children which were loaded.
          */
-        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children) {
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
         }
 
         /**
          * Called when the list of children is loaded or updated.
          *
          * @param parentId The media id of the parent media item.
-         * @param children The children which were loaded, or null if the id is invalid.
+         * @param children The children which were loaded.
          * @param options A bundle of service-specific arguments to send to the media
          *            browse service. The contents of this bundle may affect the
          *            information returned when browsing.
          */
-        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children,
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
                 @NonNull Bundle options) {
         }
 
@@ -750,10 +778,15 @@
 
             @Override
             public void onItemLoaded(Parcel itemParcel) {
-                itemParcel.setDataPosition(0);
-                MediaItem item = MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
-                itemParcel.recycle();
-                ItemCallback.this.onItemLoaded(item);
+                if (itemParcel == null) {
+                    ItemCallback.this.onItemLoaded(null);
+                } else {
+                    itemParcel.setDataPosition(0);
+                    MediaItem item =
+                            MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
+                    itemParcel.recycle();
+                    ItemCallback.this.onItemLoaded(item);
+                }
             }
 
             @Override
@@ -763,6 +796,32 @@
         }
     }
 
+    /**
+     * Callback for receiving the result of {@link #search}.
+     */
+    public abstract static class SearchCallback {
+        /**
+         * Called when the {@link #search} finished successfully.
+         *
+         * @param query The search query sent for the search request to the connected service.
+         * @param extras The bundle of service-specific arguments sent to the connected service.
+         * @param items The list of media items which contains the search result.
+         */
+        public void onSearchResult(@NonNull String query, Bundle extras,
+                @NonNull List<MediaItem> items) {
+        }
+
+        /**
+         * Called when an error happens while {@link #search} or the connected service doesn't
+         * support {@link #search}.
+         *
+         * @param query The search query sent for the search request to the connected service.
+         * @param extras The bundle of service-specific arguments sent to the connected service.
+         */
+        public void onError(@NonNull String query, Bundle extras) {
+        }
+    }
+
     interface MediaBrowserImpl {
         void connect();
         void disconnect();
@@ -775,6 +834,7 @@
                 @NonNull SubscriptionCallback callback);
         void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
         void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb);
+        void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
     }
 
     interface MediaBrowserServiceCallbackImpl {
@@ -979,13 +1039,14 @@
                 sub = new Subscription();
                 mSubscriptions.put(parentId, sub);
             }
-            sub.putCallback(options, callback);
+            Bundle copiedOptions = options == null ? null : new Bundle(options);
+            sub.putCallback(copiedOptions, callback);
 
             // If we are connected, tell the service that we are watching. If we aren't
             // connected, the service will be told when we connect.
             if (mState == CONNECT_STATE_CONNECTED) {
                 try {
-                    mServiceBinderWrapper.addSubscription(parentId, callback.mToken, options,
+                    mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
                             mCallbacksMessenger);
                 } catch (RemoteException e) {
                     // Process is crashing. We will disconnect, and upon reconnect we will
@@ -1067,6 +1128,34 @@
         }
 
         @Override
+        public void search(@NonNull final String query, final Bundle extras,
+                @NonNull final SearchCallback callback) {
+            if (!isConnected()) {
+                Log.i(TAG, "Not connected, unable to search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+
+            ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+            try {
+                mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+            } catch (RemoteException e) {
+                Log.i(TAG, "Remote error searching items with query: " + query, e);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+            }
+        }
+
+        @Override
         public void onServiceConnected(final Messenger callback, final String root,
                 final MediaSessionCompat.Token session, final Bundle extra) {
             // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
@@ -1141,7 +1230,6 @@
                 return;
             }
 
-            List<MediaItem> data = list;
             if (DEBUG) {
                 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
             }
@@ -1159,9 +1247,17 @@
             SubscriptionCallback subscriptionCallback = subscription.getCallback(options);
             if (subscriptionCallback != null) {
                 if (options == null) {
-                    subscriptionCallback.onChildrenLoaded(parentId, data);
+                    if (list == null) {
+                        subscriptionCallback.onError(parentId);
+                    } else {
+                        subscriptionCallback.onChildrenLoaded(parentId, list);
+                    }
                 } else {
-                    subscriptionCallback.onChildrenLoaded(parentId, data, options);
+                    if (list == null) {
+                        subscriptionCallback.onError(parentId, options);
+                    } else {
+                        subscriptionCallback.onChildrenLoaded(parentId, list, options);
+                    }
                 }
             }
         }
@@ -1338,9 +1434,9 @@
 
         public MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
-            // Do not send the client version for API 25 and higher, since we don't need to use
-            // EXTRA_MESSENGER_BINDER for API 24 and higher.
-            if (Build.VERSION.SDK_INT < 25) {
+            // Do not send the client version for API 26 and higher, since we don't need to use
+            // EXTRA_MESSENGER_BINDER for API 26 and higher.
+            if (Build.VERSION.SDK_INT <= 25) {
                 if (rootHints == null) {
                     rootHints = new Bundle();
                 }
@@ -1410,15 +1506,18 @@
                 mSubscriptions.put(parentId, sub);
             }
             callback.setSubscription(sub);
-            sub.putCallback(options, callback);
+            Bundle copiedOptions = options == null ? null : new Bundle(options);
+            sub.putCallback(copiedOptions, callback);
 
             if (mServiceBinderWrapper == null) {
+                // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
+                // subscribe with options won't work properly.
                 MediaBrowserCompatApi21.subscribe(
                         mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
             } else {
                 try {
                     mServiceBinderWrapper.addSubscription(
-                            parentId, callback.mToken, options, mCallbacksMessenger);
+                            parentId, callback.mToken, copiedOptions, mCallbacksMessenger);
                 } catch (RemoteException e) {
                     // Process is crashing. We will disconnect, and upon reconnect we will
                     // automatically reregister. So nothing to do here.
@@ -1524,6 +1623,45 @@
         }
 
         @Override
+        public void search(@NonNull final String query, final Bundle extras,
+                @NonNull final SearchCallback callback) {
+            if (!isConnected()) {
+                Log.i(TAG, "Not connected, unable to search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+            if (mServiceBinderWrapper == null) {
+                Log.i(TAG, "The connected service doesn't support search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Default framework implementation.
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+
+            ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+            try {
+                mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+            } catch (RemoteException e) {
+                Log.i(TAG, "Remote error searching items with query: " + query, e);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+            }
+        }
+
+        @Override
         public void onConnected() {
             Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
             if (extras == null) {
@@ -1584,9 +1722,17 @@
             SubscriptionCallback subscriptionCallback = subscription.getCallback(options);
             if (subscriptionCallback != null) {
                 if (options == null) {
-                    subscriptionCallback.onChildrenLoaded(parentId, list);
+                    if (list == null) {
+                        subscriptionCallback.onError(parentId);
+                    } else {
+                        subscriptionCallback.onChildrenLoaded(parentId, list);
+                    }
                 } else {
-                    subscriptionCallback.onChildrenLoaded(parentId, list, options);
+                    if (list == null) {
+                        subscriptionCallback.onError(parentId, options);
+                    } else {
+                        subscriptionCallback.onChildrenLoaded(parentId, list, options);
+                    }
                 }
             }
         }
@@ -1608,6 +1754,7 @@
         }
     }
 
+    // TODO: Rename to MediaBrowserImplApi26 once O is released
     static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 {
         public MediaBrowserImplApi24(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
@@ -1782,6 +1929,15 @@
             sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
         }
 
+        void search(String query, Bundle extras, ResultReceiver receiver,
+                Messenger callbacksMessenger) throws RemoteException {
+            Bundle data = new Bundle();
+            data.putString(DATA_SEARCH_QUERY, query);
+            data.putBundle(DATA_SEARCH_EXTRAS, extras);
+            data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+            sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
+        }
+
         private void sendRequest(int what, Bundle data, Messenger cbMessenger)
                 throws RemoteException {
             Message msg = Message.obtain();
@@ -1808,7 +1964,7 @@
             if (resultData != null) {
                 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
             }
-            if (resultCode != 0 || resultData == null
+            if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
                     || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
                 mCallback.onError(mMediaId);
                 return;
@@ -1821,4 +1977,37 @@
             }
         }
     }
+
+    private static class SearchResultReceiver extends ResultReceiver {
+        private final String mQuery;
+        private final Bundle mExtras;
+        private final SearchCallback mCallback;
+
+        SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
+                Handler handler) {
+            super(handler);
+            mQuery = query;
+            mExtras = extras;
+            mCallback = callback;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
+                    || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
+                mCallback.onError(mQuery, mExtras);
+                return;
+            }
+            Parcelable[] items = resultData.getParcelableArray(
+                    MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
+            List<MediaItem> results = null;
+            if (items != null) {
+                results = new ArrayList<>();
+                for (Parcelable item : items) {
+                    results.add((MediaItem) item);
+                }
+            }
+            mCallback.onSearchResult(mQuery, mExtras, results);
+        }
+    }
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
index d2d12a7..2401d09 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
@@ -29,6 +29,8 @@
     public static final String DATA_PACKAGE_NAME = "data_package_name";
     public static final String DATA_RESULT_RECEIVER = "data_result_receiver";
     public static final String DATA_ROOT_HINTS = "data_root_hints";
+    public static final String DATA_SEARCH_EXTRAS = "data_search_extras";
+    public static final String DATA_SEARCH_QUERY = "data_search_query";
 
     public static final String EXTRA_CLIENT_VERSION = "extra_client_version";
     public static final String EXTRA_SERVICE_VERSION = "extra_service_version";
@@ -156,4 +158,16 @@
      * - replyTo : Callback messenger
      */
     public static final int CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER = 7;
+
+    /** (client v1)
+     * Sent to retrieve a specific media item from the connected service.
+     * - arg1 : The client version
+     * - data
+     *     DATA_SEARCH_QUERY : A string for search query that contains keywords separated by space.
+     *     DATA_SEARCH_EXTRAS : A bundle of service-specific arguments to send to the media browser
+     *                          service.
+     *     DATA_RESULT_RECEIVER : Result receiver to get the result
+     * - replyTo : Callback messenger
+     */
+    public static final int CLIENT_MSG_SEARCH = 8;
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index de5047b..9c65ce6 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -23,6 +23,7 @@
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
@@ -33,6 +34,8 @@
 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
@@ -116,14 +119,26 @@
     @RestrictTo(LIBRARY_GROUP)
     public static final String KEY_MEDIA_ITEM = "media_item";
 
-    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
-    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002;
+    /**
+     * A key for passing the list of MediaItems to the ResultReceiver in search.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String KEY_SEARCH_RESULTS = "search_results";
+
+    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
+    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
+    static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
+
+    static final int RESULT_ERROR = -1;
+    static final int RESULT_OK = 0;
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
-            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
+            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
     private @interface ResultFlags { }
 
     final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
@@ -355,6 +370,7 @@
         }
     }
 
+    // TODO: Rename to MediaBrowserServiceImplApi26 once O is released
     class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
             MediaBrowserServiceCompatApi24.ServiceCompatProxy {
         @Override
@@ -453,6 +469,12 @@
                 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
                     mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
                     break;
+                case CLIENT_MSG_SEARCH:
+                    mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY),
+                            data.getBundle(DATA_SEARCH_EXTRAS),
+                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
+                            new ServiceCallbacksCompat(msg.replyTo));
+                    break;
                 default:
                     Log.w(TAG, "Unhandled message: " + msg
                             + "\n  Service version: " + SERVICE_VERSION_CURRENT
@@ -719,6 +741,27 @@
                 }
             });
         }
+
+        public void search(final String query, final Bundle extras, final ResultReceiver receiver,
+                final ServiceCallbacks callbacks) {
+            if (TextUtils.isEmpty(query) || receiver == null) {
+                return;
+            }
+
+            mHandler.postOrRun(new Runnable() {
+                @Override
+                public void run() {
+                    final IBinder b = callbacks.asBinder();
+
+                    ConnectionRecord connection = mConnections.get(b);
+                    if (connection == null) {
+                        Log.w(TAG, "search for callback that isn't registered query=" + query);
+                        return;
+                    }
+                    performSearch(query, extras, connection, receiver);
+                }
+            });
+        }
     }
 
     private interface ServiceCallbacks {
@@ -786,7 +829,7 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+        if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
             mImpl = new MediaBrowserServiceImplApi24();
         } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserServiceImplApi23();
@@ -828,7 +871,6 @@
      * @see BrowserRoot#EXTRA_RECENT
      * @see BrowserRoot#EXTRA_OFFLINE
      * @see BrowserRoot#EXTRA_SUGGESTED
-     * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
      */
     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
             int clientUid, @Nullable Bundle rootHints);
@@ -842,11 +884,16 @@
      * {@link Result#detach result.detach} may be called before returning from
      * this function, and then {@link Result#sendResult result.sendResult}
      * called when the loading is complete.
+     * </p><p>
+     * In case the media item does not have any children, call {@link Result#sendResult}
+     * with an empty list. When the given {@code parentId} is invalid, implementations must
+     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
+     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
+     * </p>
      *
      * @param parentId The id of the parent media item whose children are to be
      *            queried.
-     * @param result The Result to send the list of children to, or null if the
-     *            id is invalid.
+     * @param result The Result to send the list of children to.
      */
     public abstract void onLoadChildren(@NonNull String parentId,
             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
@@ -860,11 +907,16 @@
      * {@link Result#detach result.detach} may be called before returning from
      * this function, and then {@link Result#sendResult result.sendResult}
      * called when the loading is complete.
+     * </p><p>
+     * In case the media item does not have any children, call {@link Result#sendResult}
+     * with an empty list. When the given {@code parentId} is invalid, implementations must
+     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
+     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
+     * </p>
      *
      * @param parentId The id of the parent media item whose children are to be
      *            queried.
-     * @param result The Result to send the list of children to, or null if the
-     *            id is invalid.
+     * @param result The Result to send the list of children to.
      * @param options A bundle of service-specific arguments sent from the media
      *            browse. The information returned through the result should be
      *            affected by the contents of this bundle.
@@ -896,12 +948,39 @@
      * @param result The Result to send the item to, or null if the id is
      *            invalid.
      */
-    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
+    public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) {
         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
         result.sendResult(null);
     }
 
     /**
+     * Called to get the search result.
+     * <p>
+     * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be
+     * an expensive operation {@link Result#detach result.detach} may be called before returning
+     * from this function, and then {@link Result#sendResult result.sendResult} called when the
+     * search has been completed.
+     * </p><p>
+     * In case there are no search results, call {@link Result#sendResult result.sendResult} with an
+     * empty list. In case there are some errors happened, call {@link Result#sendResult
+     * result.sendResult} with {@code null}, which will invoke {@link
+     * MediaBrowserCompat.SearchCallback#onError}.
+     * </p><p>
+     * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
+     * </p>
+     *
+     * @param query The search query sent from the media browser. It contains keywords separated
+     *            by space.
+     * @param extras The bundle of service-specific arguments sent from the media browser.
+     * @param result The {@link Result} to send the search result.
+     */
+    public void onSearch(@NonNull String query, Bundle extras,
+            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+        result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
+        result.sendResult(null);
+    }
+
+    /**
      * Call to set the media session.
      * <p>
      * This should be called as soon as possible during the service's startup.
@@ -942,7 +1021,6 @@
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
-     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
      */
     public final Bundle getBrowserRootHints() {
         return mImpl.getBrowserRootHints();
@@ -1122,12 +1200,12 @@
                     @Override
                     void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
                         if ((flags & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
-                            receiver.send(-1, null);
+                            receiver.send(RESULT_ERROR, null);
                             return;
                         }
                         Bundle bundle = new Bundle();
                         bundle.putParcelable(KEY_MEDIA_ITEM, item);
-                        receiver.send(0, bundle);
+                        receiver.send(RESULT_OK, bundle);
                     }
                 };
 
@@ -1141,6 +1219,34 @@
         }
     }
 
+    void performSearch(final String query, Bundle extras, ConnectionRecord connection,
+            final ResultReceiver receiver) {
+        final Result<List<MediaBrowserCompat.MediaItem>> result =
+                new Result<List<MediaBrowserCompat.MediaItem>>(query) {
+            @Override
+            void onResultSent(List<MediaBrowserCompat.MediaItem> items, @ResultFlags int flag) {
+                if ((flag & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0
+                        || items == null) {
+                    receiver.send(RESULT_ERROR, null);
+                    return;
+                }
+                Bundle bundle = new Bundle();
+                bundle.putParcelableArray(KEY_SEARCH_RESULTS,
+                        items.toArray(new MediaBrowserCompat.MediaItem[0]));
+                receiver.send(RESULT_OK, bundle);
+            }
+        };
+
+        mCurConnection = connection;
+        onSearch(query, extras, result);
+        mCurConnection = null;
+
+        if (!result.isDone()) {
+            throw new IllegalStateException("onSearch must call detach() or sendResult()"
+                    + " before returning for query=" + query);
+        }
+    }
+
     /**
      * Contains information that the browser service needs to send to the client
      * when first connected.
@@ -1159,7 +1265,6 @@
          *
          * @see #EXTRA_OFFLINE
          * @see #EXTRA_SUGGESTED
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
 
@@ -1177,7 +1282,6 @@
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_SUGGESTED
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
 
@@ -1196,7 +1300,6 @@
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_OFFLINE
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
 
@@ -1217,7 +1320,10 @@
          * @see #EXTRA_RECENT
          * @see #EXTRA_OFFLINE
          * @see #EXTRA_SUGGESTED
+         * @deprecated Use {@link MediaBrowserCompat#search(String, Bundle,
+         *             MediaBrowserCompat.SearchCallback)} instead.
          */
+        @Deprecated
         public static final String EXTRA_SUGGESTION_KEYWORDS
                 = "android.service.media.extra.SUGGESTION_KEYWORDS";
 
diff --git a/media-compat/java/android/support/v4/media/MediaDescriptionCompat.aidl b/media-compat/java/android/support/v4/media/MediaDescriptionCompat.aidl
new file mode 100644
index 0000000..f002cdd
--- /dev/null
+++ b/media-compat/java/android/support/v4/media/MediaDescriptionCompat.aidl
@@ -0,0 +1,18 @@
+/* Copyright 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;
+
+parcelable MediaDescriptionCompat;
diff --git a/media-compat/java/android/support/v4/media/MediaMetadataCompat.java b/media-compat/java/android/support/v4/media/MediaMetadataCompat.java
index 8792982..f8c1f57 100644
--- a/media-compat/java/android/support/v4/media/MediaMetadataCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaMetadataCompat.java
@@ -238,6 +238,13 @@
             = "android.media.metadata.BT_FOLDER_TYPE";
 
     /**
+     * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A
+     * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set
+     * to 0 by default.
+     */
+    public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+
+    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -255,7 +262,8 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
-            METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE})
+            METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE,
+            METADATA_KEY_ADVERTISEMENT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface LongKey {}
 
@@ -312,6 +320,7 @@
         METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
         METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
         METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
     }
 
     private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
@@ -762,6 +771,7 @@
          * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
          * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
          * <li>{@link #METADATA_KEY_YEAR}</li>
+         * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
          * </ul>
          *
          * @param key The key for referencing this value
diff --git a/media-compat/java/android/support/v4/media/TransportController.java b/media-compat/java/android/support/v4/media/TransportController.java
index b92a4a1..d1d2649 100644
--- a/media-compat/java/android/support/v4/media/TransportController.java
+++ b/media-compat/java/android/support/v4/media/TransportController.java
@@ -16,64 +16,111 @@
 
 package android.support.v4.media;
 
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
 /**
  * Base interface to controlling a media transport.  This is the
  * interface for implementing things like on-screen controls: it
  * allows them to request changes in playback, retrieve the current
  * playback state, and monitor for changes to the playback state.
+ *
+ * @deprecated Use {@link MediaControllerCompat}.
  */
+@Deprecated
 public abstract class TransportController {
     /**
-     * Start listening to changes in playback state.
+     * @deprecated Use {@link MediaControllerCompat}.
      */
+    @Deprecated
+    public TransportController() {
+    }
+
+    /**
+     * Start listening to changes in playback state.
+     *
+     * @deprecated Use
+     *         {@link MediaControllerCompat#registerCallback(MediaControllerCompat.Callback)}.
+     */
+    @Deprecated
     public abstract void registerStateListener(TransportStateListener listener);
 
     /**
      * Stop listening to changes in playback state.
+     *
+     * @deprecated Use
+     *         {@link MediaControllerCompat#unregisterCallback(MediaControllerCompat.Callback)}.
      */
+    @Deprecated
     public abstract void unregisterStateListener(TransportStateListener listener);
 
     /**
      * Request that the player start its playback at its current position.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#play}.
      */
+    @Deprecated
     public abstract void startPlaying();
 
     /**
      * Request that the player pause its playback and stay at its current position.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#pause}.
      */
+    @Deprecated
     public abstract void pausePlaying();
 
     /**
      * Request that the player stop its playback; it may clear its state in whatever
      * way is appropriate.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#stop}.
      */
+    @Deprecated
     public abstract void stopPlaying();
 
     /**
      * Retrieve the total duration of the media stream, in milliseconds.
+     *
+     * @deprecated Use {@link MediaMetadataCompat#METADATA_KEY_DURATION}.
      */
+    @Deprecated
     public abstract long getDuration();
 
     /**
      * Retrieve the current playback location in the media stream, in milliseconds.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getPosition} and
+     *         {@link PlaybackStateCompat#getLastPositionUpdateTime}.
      */
+    @Deprecated
     public abstract long getCurrentPosition();
 
     /**
      * Move to a new location in the media stream.
      * @param pos Position to move to, in milliseconds.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#seekTo}.
      */
+    @Deprecated
     public abstract void seekTo(long pos);
 
     /**
      * Return whether the player is currently playing its stream.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getState}.
      */
+    @Deprecated
     public abstract boolean isPlaying();
 
     /**
      * Retrieve amount, in percentage (0-100), that the media stream has been buffered
      * on to the local device.  Return 100 if the stream is always local.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getBufferedPosition} and
+     *         {@link MediaMetadataCompat#METADATA_KEY_DURATION}.
      */
+    @Deprecated
     public abstract int getBufferPercentage();
 
     /**
@@ -87,6 +134,9 @@
      *      {@link TransportMediator#FLAG_KEY_MEDIA_STOP},
      *      {@link TransportMediator#FLAG_KEY_MEDIA_FAST_FORWARD},
      *      {@link TransportMediator#FLAG_KEY_MEDIA_NEXT}
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getActions}.
      */
+    @Deprecated
     public abstract int getTransportControlFlags();
 }
diff --git a/media-compat/java/android/support/v4/media/TransportMediator.java b/media-compat/java/android/support/v4/media/TransportMediator.java
index ec3baec..40c245c 100644
--- a/media-compat/java/android/support/v4/media/TransportMediator.java
+++ b/media-compat/java/android/support/v4/media/TransportMediator.java
@@ -19,7 +19,10 @@
 import android.app.Activity;
 import android.content.Context;
 import android.media.AudioManager;
+import android.media.RemoteControlClient;
 import android.os.Build;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
 import android.view.KeyEvent;
 import android.view.View;
 
@@ -43,7 +46,10 @@
  *
  * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java
  *      complete}
+ *
+ * @deprecated Use {@link MediaControllerCompat}.
  */
+@Deprecated
 public class TransportMediator extends TransportController {
     final Context mContext;
     final TransportPerformer mCallbacks;
@@ -75,36 +81,91 @@
         }
     };
 
-    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY} */
+    /**
+     * Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY}
+     *
+     * @deprecated Use {@link KeyEvent#KEYCODE_MEDIA_PLAY}.
+     */
+    @Deprecated
     public static final int KEYCODE_MEDIA_PLAY = 126;
-    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE} */
+    /**
+     * Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE}
+     *
+     * @deprecated Use {@link KeyEvent#KEYCODE_MEDIA_PAUSE}.
+     */
+    @Deprecated
     public static final int KEYCODE_MEDIA_PAUSE = 127;
-    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD} */
+    /**
+     * Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD}
+     *
+     * @deprecated Use {@link KeyEvent#KEYCODE_MEDIA_RECORD}.
+     */
+    @Deprecated
     public static final int KEYCODE_MEDIA_RECORD = 130;
 
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
-     * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
+     * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
-     * RemoteControlClient.FLAG_KEY_MEDIA_REWIND} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
+     * RemoteControlClient.FLAG_KEY_MEDIA_REWIND}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_REWIND}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
-     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
+     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_PLAY}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
-     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
+     * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
-     * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
+     * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_PAUSE}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
-     * RemoteControlClient.FLAG_KEY_MEDIA_STOP} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
+     * RemoteControlClient.FLAG_KEY_MEDIA_STOP}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_STOP}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
-     * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
+     * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
-    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
-     * RemoteControlClient.FLAG_KEY_MEDIA_NEXT} */
+    /**
+     * Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
+     * RemoteControlClient.FLAG_KEY_MEDIA_NEXT}
+     *
+     * @deprecated Use {@link RemoteControlClient#FLAG_KEY_MEDIA_NEXT}.
+     */
+    @Deprecated
     public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
 
     static boolean isMediaKey(int keyCode) {
@@ -148,10 +209,18 @@
         }
     };
 
+    /**
+     * @deprecated Use {@link MediaControllerCompat}.
+     */
+    @Deprecated
     public TransportMediator(Activity activity, TransportPerformer callbacks) {
         this(activity, null, callbacks);
     }
 
+    /**
+     * @deprecated Use {@link MediaControllerCompat}.
+     */
+    @Deprecated
     public TransportMediator(View view, TransportPerformer callbacks) {
         this(null, view, callbacks);
     }
@@ -185,7 +254,10 @@
      * you will interact with these through
      * {@link TransportPerformer#onGetCurrentPosition() TransportPerformer.onGetCurrentPosition} and
      * {@link TransportPerformer#onSeekTo TransportPerformer.onSeekTo}, respectively.</p>
+     *
+     * @deprecated Use {@link MediaControllerCompat}.
      */
+    @Deprecated
     public Object getRemoteControlClient() {
         return mController != null ? mController.getRemoteControlClient() : null;
     }
@@ -195,16 +267,33 @@
      * the transport an opportunity to intercept media keys.  Any such keys will show up
      * in {@link TransportPerformer}.
      * @param event
+     *
+     * @deprecated Use {@link MediaControllerCompat#dispatchMediaButtonEvent}.
      */
+    @Deprecated
     public boolean dispatchKeyEvent(KeyEvent event) {
         return event.dispatch(mKeyEventCallback, (KeyEvent.DispatcherState) mDispatcherState, this);
     }
 
+    /**
+     * Start listening to changes in playback state.
+     *
+     * @deprecated Use
+     *         {@link MediaControllerCompat#registerCallback(MediaControllerCompat.Callback)}.
+     */
+    @Deprecated
     @Override
     public void registerStateListener(TransportStateListener listener) {
         mListeners.add(listener);
     }
 
+    /**
+     * Stop listening to changes in playback state.
+     *
+     * @deprecated Use
+     *         {@link MediaControllerCompat#unregisterCallback(MediaControllerCompat.Callback)}.
+     */
+    @Deprecated
     @Override
     public void unregisterStateListener(TransportStateListener listener) {
         mListeners.remove(listener);
@@ -245,6 +334,10 @@
         }
     }
 
+    /**
+     * @deprecated Not needed when you use {@link MediaControllerCompat}.
+     */
+    @Deprecated
     public void refreshState() {
         pushControllerState();
         reportPlayingChanged();
@@ -254,7 +347,10 @@
     /**
      * Move the controller into the playing state.  This updates the remote control
      * client to indicate it is playing, and takes audio focus for the app.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#play}.
      */
+    @Deprecated
     @Override
     public void startPlaying() {
         if (mController != null) {
@@ -268,7 +364,10 @@
     /**
      * Move the controller into the paused state.  This updates the remote control
      * client to indicate it is paused, but keeps audio focus.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#pause}.
      */
+    @Deprecated
     @Override
     public void pausePlaying() {
         if (mController != null) {
@@ -282,7 +381,10 @@
     /**
      * Move the controller into the stopped state.  This updates the remote control
      * client to indicate it is stopped, and removes audio focus from the app.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#stop}.
      */
+    @Deprecated
     @Override
     public void stopPlaying() {
         if (mController != null) {
@@ -293,33 +395,67 @@
         reportPlayingChanged();
     }
 
+    /**
+     * Retrieve the total duration of the media stream, in milliseconds.
+     *
+     * @deprecated Use {@link MediaMetadataCompat#METADATA_KEY_DURATION}.
+     */
+    @Deprecated
     @Override
     public long getDuration() {
         return mCallbacks.onGetDuration();
     }
 
+    /**
+     * Retrieve the current playback location in the media stream, in milliseconds.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getPosition} and
+     *         {@link PlaybackStateCompat#getLastPositionUpdateTime}.
+     */
+    @Deprecated
     @Override
     public long getCurrentPosition() {
         return mCallbacks.onGetCurrentPosition();
     }
 
+    /**
+     * Move to a new location in the media stream.
+     * @param pos Position to move to, in milliseconds.
+     *
+     * @deprecated Use {@link MediaControllerCompat.TransportControls#seekTo}.
+     */
+    @Deprecated
     @Override
     public void seekTo(long pos) {
         mCallbacks.onSeekTo(pos);
     }
 
+    /**
+     * Return whether the player is currently playing its stream.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getState}.
+     */
+    @Deprecated
     @Override
     public boolean isPlaying() {
         return mCallbacks.onIsPlaying();
     }
 
+    /**
+     * Retrieve amount, in percentage (0-100), that the media stream has been buffered
+     * on to the local device.  Return 100 if the stream is always local.
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getBufferedPosition} and
+     *         {@link MediaMetadataCompat#METADATA_KEY_DURATION}.
+     */
+    @Deprecated
     @Override
     public int getBufferPercentage() {
         return mCallbacks.onGetBufferPercentage();
     }
 
     /**
-     * Retrieves the flags for the media transport control buttons that this transport supports.
+     * Retrieve the flags for the media transport control buttons that this transport supports.
      * Result is a combination of the following flags:
      *      {@link #FLAG_KEY_MEDIA_PREVIOUS},
      *      {@link #FLAG_KEY_MEDIA_REWIND},
@@ -329,7 +465,10 @@
      *      {@link #FLAG_KEY_MEDIA_STOP},
      *      {@link #FLAG_KEY_MEDIA_FAST_FORWARD},
      *      {@link #FLAG_KEY_MEDIA_NEXT}
+     *
+     * @deprecated Use {@link PlaybackStateCompat#getActions}.
      */
+    @Deprecated
     @Override
     public int getTransportControlFlags() {
         return mCallbacks.onGetTransportControlFlags();
@@ -339,7 +478,10 @@
      * Optionally call when no longer using the TransportController.  Its resources
      * will also be automatically cleaned up when your activity/view is detached from
      * its window, so you don't normally need to call this explicitly.
+     *
+     * @deprecated Not needed when you use {@link MediaControllerCompat}.
      */
+    @Deprecated
     public void destroy() {
         mController.destroy();
     }
diff --git a/media-compat/java/android/support/v4/media/TransportPerformer.java b/media-compat/java/android/support/v4/media/TransportPerformer.java
index 2d3afc9..79e4869 100644
--- a/media-compat/java/android/support/v4/media/TransportPerformer.java
+++ b/media-compat/java/android/support/v4/media/TransportPerformer.java
@@ -17,6 +17,8 @@
 package android.support.v4.media;
 
 import android.os.SystemClock;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat.Builder;
 import android.view.KeyEvent;
 
 /**
@@ -25,45 +27,77 @@
  * requests may either come from key events dispatched directly to your UI, or
  * events sent over a media button event receiver that this class keeps active
  * while your window is in focus.
+ *
+ * @deprecated Use {@link MediaSessionCompat}.
  */
+@Deprecated
 public abstract class TransportPerformer {
     /**
+     * @deprecated Use {@link MediaSessionCompat}.
+     */
+    @Deprecated
+    public TransportPerformer() {
+    }
+
+    /**
      * Request to start playback on the media, resuming from whatever current state
      * (position etc) it is in.
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onPlay}.
      */
+    @Deprecated
     public abstract void onStart();
 
     /**
      * Request to pause playback of the media, staying at the current playback position
      * and other state so a later call to {@link #onStart()} will resume at the same place.
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onPause}.
      */
+    @Deprecated
     public abstract void onPause();
 
     /**
      * Request to completely stop playback of the media, clearing whatever state the
      * player thinks is appropriate.
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onStop}.
      */
+    @Deprecated
     public abstract void onStop();
 
     /**
      * Request to return the duration of the current media, in milliseconds.
+     *
+     * @deprecated Use {@link MediaMetadataCompat.Builder#putLong} with
+     *         {@link MediaMetadataCompat#METADATA_KEY_DURATION}.
      */
+    @Deprecated
     public abstract long onGetDuration();
 
     /**
      * Request to return the current playback position, in milliseconds.
+     *
+     * @deprecated Use {@link Builder#setState(int, long, float)}.
      */
+    @Deprecated
     public abstract long onGetCurrentPosition();
 
     /**
      * Request to move the current playback position.
      * @param pos New position to move to, in milliseconds.
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onSeekTo}.
      */
+    @Deprecated
     public abstract void onSeekTo(long pos);
 
     /**
      * Request to find out whether the player is currently playing its media.
+     *
+     * @deprecated Use {@link Builder#setState(int, long, float)}.
      */
+    @Deprecated
     public abstract boolean onIsPlaying();
 
     /**
@@ -71,7 +105,10 @@
      * @return Return a percentage (0-100) indicating how much of the total data
      * has been buffered.  The default implementation returns 100, meaning the content
      * is always on the local device.
+     *
+     * @deprecated Use {@link Builder#setBufferedPosition}.
      */
+    @Deprecated
     public int onGetBufferPercentage() {
         return 100;
     }
@@ -93,7 +130,10 @@
      *      {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE},
      *      {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, and
      *      {@link TransportMediator#FLAG_KEY_MEDIA_STOP}</p>
+     *
+     * @deprecated Use {@link Builder#setActions}.
      */
+    @Deprecated
     public int onGetTransportControlFlags() {
         return TransportMediator.FLAG_KEY_MEDIA_PLAY
                 | TransportMediator.FLAG_KEY_MEDIA_PLAY_PAUSE
@@ -122,7 +162,10 @@
      * continues on to its default key handling (which for media keys means
      * being delivered to the current media remote control, which should
      * be us).
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onMediaButtonEvent}.
      */
+    @Deprecated
     public boolean onMediaButtonDown(int keyCode, KeyEvent event) {
         switch (keyCode) {
             case TransportMediator.KEYCODE_MEDIA_PLAY:
@@ -159,7 +202,10 @@
      * continues on to its default key handling (which for media keys means
      * being delivered to the current media remote control, which should
      * be us).
+     *
+     * @deprecated Use {@link MediaSessionCompat.Callback#onMediaButtonEvent}.
      */
+    @Deprecated
     public boolean onMediaButtonUp(int keyCode, KeyEvent event) {
         return true;
     }
@@ -183,7 +229,10 @@
      * OnAudioFocusChangeListener.onAudioFocusChange}.  The default implementation will
      * deliver a {@link KeyEvent#KEYCODE_MEDIA_STOP}
      * when receiving {@link android.media.AudioManager#AUDIOFOCUS_LOSS}.
+     *
+     * @deprecated You must implement your own audio focus handler.
      */
+    @Deprecated
     public void onAudioFocusChange(int focusChange) {
         int keyCode = 0;
         switch (focusChange) {
diff --git a/media-compat/java/android/support/v4/media/TransportStateListener.java b/media-compat/java/android/support/v4/media/TransportStateListener.java
index 384cca2..554983e 100644
--- a/media-compat/java/android/support/v4/media/TransportStateListener.java
+++ b/media-compat/java/android/support/v4/media/TransportStateListener.java
@@ -16,16 +16,31 @@
 
 package android.support.v4.media;
 
+import android.support.v4.media.session.MediaControllerCompat.Callback;
+
 /**
  * A listener for playback changes that can be registered with
  * {@link TransportController}.
+ *
+ * @deprecated Use {@link Callback}.
  */
+@Deprecated
 public class TransportStateListener {
     /**
+     * @deprecated Use {@link Callback}.
+     */
+    @Deprecated
+    public TransportStateListener() {
+    }
+
+    /**
      * The play state of the transport changed.  Use
      * {@link android.support.v4.media.TransportController#isPlaying()
      * TransportController.isPlaying()} to determine the new state.
+     *
+     * @deprecated Use {@link Callback#onPlaybackStateChanged}.
      */
+    @Deprecated
     public void onPlayingChanged(TransportController controller) {
     }
 
@@ -33,7 +48,10 @@
      * The available controls of the transport changed.  Use
      * {@link TransportController#getTransportControlFlags()}
      * TransportController.getTransportControlFlags()} to determine the new state.
+     *
+     * @deprecated Use {@link Callback#onPlaybackStateChanged}.
      */
+    @Deprecated
     public void onTransportControlsChanged(TransportController controller) {
     }
 }
diff --git a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
index d905350..d1d143d 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
@@ -37,4 +37,6 @@
     void onQueueTitleChanged(CharSequence title);
     void onExtrasChanged(in Bundle extras);
     void onVolumeInfoChanged(in ParcelableVolumeInfo info);
+    void onRepeatModeChanged(int repeatMode);
+    void onShuffleModeChanged(boolean enabled);
 }
diff --git a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
index c7705e8..969b803 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
@@ -17,6 +17,7 @@
 
 import android.app.PendingIntent;
 import android.content.Intent;
+import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.RatingCompat;
 import android.support.v4.media.session.IMediaControllerCallback;
@@ -33,7 +34,7 @@
  * @hide
  */
 interface IMediaSession {
-    // Next ID: 40
+    // Next ID: 44
     void sendCommand(String command, in Bundle args, in MediaSessionCompat.ResultReceiverWrapper cb) = 0;
     boolean sendMediaButton(in KeyEvent mediaButton) = 1;
     void registerCallbackListener(in IMediaControllerCallback cb) = 2;
@@ -52,6 +53,12 @@
     CharSequence getQueueTitle() = 29;
     Bundle getExtras() = 30;
     int getRatingType() = 31;
+    int getRepeatMode() = 36;
+    boolean isShuffleModeEnabled() = 37;
+    void addQueueItem(in MediaDescriptionCompat description) = 40;
+    void addQueueItemAt(in MediaDescriptionCompat description, int index) = 41;
+    void removeQueueItem(in MediaDescriptionCompat description) = 42;
+    void removeQueueItemAt(int index) = 43;
 
     // These commands are for the TransportControls
     void prepare() = 32;
@@ -71,5 +78,7 @@
     void rewind() = 22;
     void seekTo(long pos) = 23;
     void rate(in RatingCompat rating) = 24;
+    void setRepeatMode(int repeatMode) = 38;
+    void setShuffleModeEnabled(boolean shuffleMode) = 39;
     void sendCustomAction(String action, in Bundle args) = 25;
 }
diff --git a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
index 74658a8..2783d3b 100644
--- a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -28,8 +28,10 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.BundleCompat;
 import android.support.v4.app.SupportActivity;
+import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.RatingCompat;
 import android.support.v4.media.VolumeProviderCompat;
@@ -62,6 +64,19 @@
 
     static final String COMMAND_GET_EXTRA_BINDER =
             "android.support.v4.media.session.command.GET_EXTRA_BINDER";
+    static final String COMMAND_ADD_QUEUE_ITEM =
+            "android.support.v4.media.session.command.ADD_QUEUE_ITEM";
+    static final String COMMAND_ADD_QUEUE_ITEM_AT =
+            "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
+    static final String COMMAND_REMOVE_QUEUE_ITEM =
+            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
+    static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
+            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";
+
+    static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
+            "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
+    static final String COMMAND_ARGUMENT_INDEX =
+            "android.support.v4.media.session.command.ARGUMENT_INDEX";
 
     private static class MediaControllerExtraData extends SupportActivity.ExtraData {
         private final MediaControllerCompat mMediaController;
@@ -129,7 +144,7 @@
                 return new MediaControllerCompat(activity,
                         MediaSessionCompat.Token.fromToken(sessionTokenObj));
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getMediaController. " + e);
+                Log.e(TAG, "Dead object in getMediaController.", e);
             }
         }
         return null;
@@ -237,6 +252,62 @@
     }
 
     /**
+     * Add a queue item from the given {@code description} at the end of the play queue
+     * of this session. Not all sessions may support this.
+     *
+     * @param description The {@link MediaDescriptionCompat} for creating the
+     *            {@link MediaSessionCompat.QueueItem} to be inserted.
+     * @throws UnsupportedOperationException If this session doesn't support this.
+     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+     */
+    public void addQueueItem(MediaDescriptionCompat description) {
+        mImpl.addQueueItem(description);
+    }
+
+    /**
+     * Add a queue item from the given {@code description} at the specified position
+     * in the play queue of this session. Shifts the queue item currently at that position
+     * (if any) and any subsequent queue items to the right (adds one to their indices).
+     * Not all sessions may support this.
+     *
+     * @param description The {@link MediaDescriptionCompat} for creating the
+     *            {@link MediaSessionCompat.QueueItem} to be inserted.
+     * @param index The index at which the created {@link MediaSessionCompat.QueueItem}
+     *            is to be inserted.
+     * @throws UnsupportedOperationException If this session doesn't support this.
+     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+     */
+    public void addQueueItem(MediaDescriptionCompat description, int index) {
+        mImpl.addQueueItem(description, index);
+    }
+
+    /**
+     * Remove the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
+     * with the given {@link MediaDescriptionCompat description} in the play queue of the
+     * associated session. Not all sessions may support this.
+     *
+     * @param description The {@link MediaDescriptionCompat} for denoting the
+     *            {@link MediaSessionCompat.QueueItem} to be removed.
+     * @throws UnsupportedOperationException If this session doesn't support this.
+     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+     */
+    public void removeQueueItem(MediaDescriptionCompat description) {
+        mImpl.removeQueueItem(description);
+    }
+
+    /**
+     * Remove an queue item at the specified position in the play queue
+     * of this session. Not all sessions may support this.
+     *
+     * @param index The index of the element to be removed.
+     * @throws UnsupportedOperationException If this session doesn't support this.
+     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+     */
+    public void removeQueueItemAt(int index) {
+        mImpl.removeQueueItemAt(index);
+    }
+
+    /**
      * Get the queue title for this session.
      */
     public CharSequence getQueueTitle() {
@@ -269,6 +340,25 @@
     }
 
     /**
+     * Get the repeat mode for this session.
+     *
+     * @return The latest repeat mode set to the session, or
+     *         {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
+     */
+    public int getRepeatMode() {
+        return mImpl.getRepeatMode();
+    }
+
+    /**
+     * Return whether the shuffle mode is enabled for this session.
+     *
+     * @return {@code true} if the shuffle mode is enabled, {@code false} if disabled or not set.
+     */
+    public boolean isShuffleModeEnabled() {
+        return mImpl.isShuffleModeEnabled();
+    }
+
+    /**
      * Get the flags for this session. Flags are defined in
      * {@link MediaSessionCompat}.
      *
@@ -404,6 +494,15 @@
         return mImpl.getPackageName();
     }
 
+    @VisibleForTesting
+    boolean isExtraBinderReady() {
+        if (mImpl instanceof MediaControllerImplApi21) {
+            return ((MediaControllerImplApi21) mImpl).mExtraBinder != null;
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Gets the underlying framework
      * {@link android.media.session.MediaController} object.
@@ -510,6 +609,25 @@
         public void onAudioInfoChanged(PlaybackInfo info) {
         }
 
+        /**
+         * Override to handle changes to the repeat mode.
+         *
+         * @param repeatMode The repeat mode. It should be one of followings:
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        }
+
+        /**
+         * Override to handle changes to the shuffle mode.
+         *
+         * @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise.
+         */
+        public void onShuffleModeChanged(boolean enabled) {
+        }
+
         @Override
         public void binderDied() {
             onSessionDestroyed();
@@ -542,7 +660,7 @@
 
             @Override
             public void onPlaybackStateChanged(Object stateObj) {
-                if (mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 22) {
+                if (mHasExtraCallback) {
                     // Ignore. ExtraCallback will handle this.
                 } else {
                     Callback.this.onPlaybackStateChanged(
@@ -614,6 +732,16 @@
             }
 
             @Override
+            public void onRepeatModeChanged(int repeatMode) throws RemoteException {
+                mHandler.post(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
+            }
+
+            @Override
+            public void onShuffleModeChanged(boolean enabled) throws RemoteException {
+                mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, enabled, null);
+            }
+
+            @Override
             public void onExtrasChanged(Bundle extras) throws RemoteException {
                 mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
             }
@@ -638,6 +766,8 @@
             private static final int MSG_UPDATE_QUEUE_TITLE = 6;
             private static final int MSG_UPDATE_EXTRAS = 7;
             private static final int MSG_DESTROYED = 8;
+            private static final int MSG_UPDATE_REPEAT_MODE = 9;
+            private static final int MSG_UPDATE_SHUFFLE_MODE = 10;
 
             public MessageHandler(Looper looper) {
                 super(looper);
@@ -664,6 +794,12 @@
                     case MSG_UPDATE_QUEUE_TITLE:
                         onQueueTitleChanged((CharSequence) msg.obj);
                         break;
+                    case MSG_UPDATE_REPEAT_MODE:
+                        onRepeatModeChanged((int) msg.obj);
+                        break;
+                    case MSG_UPDATE_SHUFFLE_MODE:
+                        onShuffleModeChanged((boolean) msg.obj);
+                        break;
                     case MSG_UPDATE_EXTRAS:
                         onExtrasChanged((Bundle) msg.obj);
                         break;
@@ -839,6 +975,23 @@
         public abstract void setRating(RatingCompat rating);
 
         /**
+         * Set the repeat mode for this session.
+         *
+         * @param repeatMode The repeat mode. Must be one of the followings:
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+
+        /**
+         * Set the shuffle mode for this session.
+         *
+         * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+         */
+        public abstract void setShuffleModeEnabled(boolean enabled);
+
+        /**
          * Send a custom action for the {@link MediaSessionCompat} to perform.
          *
          * @param customAction The action to perform.
@@ -960,9 +1113,15 @@
         MediaMetadataCompat getMetadata();
 
         List<MediaSessionCompat.QueueItem> getQueue();
+        void addQueueItem(MediaDescriptionCompat description);
+        void addQueueItem(MediaDescriptionCompat description, int index);
+        void removeQueueItem(MediaDescriptionCompat description);
+        void removeQueueItemAt(int index);
         CharSequence getQueueTitle();
         Bundle getExtras();
         int getRatingType();
+        int getRepeatMode();
+        boolean isShuffleModeEnabled();
         long getFlags();
         PlaybackInfo getPlaybackInfo();
         PendingIntent getSessionActivity();
@@ -996,7 +1155,7 @@
                 callback.setHandler(handler);
                 callback.mRegistered = true;
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in registerCallback. " + e);
+                Log.e(TAG, "Dead object in registerCallback.", e);
                 callback.onSessionDestroyed();
             }
         }
@@ -1012,7 +1171,7 @@
                 mBinder.asBinder().unlinkToDeath(callback, 0);
                 callback.mRegistered = false;
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in unregisterCallback. " + e);
+                Log.e(TAG, "Dead object in unregisterCallback.", e);
             }
         }
 
@@ -1024,7 +1183,7 @@
             try {
                 mBinder.sendMediaButton(event);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in dispatchMediaButtonEvent. " + e);
+                Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e);
             }
             return false;
         }
@@ -1043,7 +1202,7 @@
             try {
                 return mBinder.getPlaybackState();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getPlaybackState. " + e);
+                Log.e(TAG, "Dead object in getPlaybackState.", e);
             }
             return null;
         }
@@ -1053,7 +1212,7 @@
             try {
                 return mBinder.getMetadata();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getMetadata. " + e);
+                Log.e(TAG, "Dead object in getMetadata.", e);
             }
             return null;
         }
@@ -1063,17 +1222,73 @@
             try {
                 return mBinder.getQueue();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getQueue. " + e);
+                Log.e(TAG, "Dead object in getQueue.", e);
             }
             return null;
         }
 
         @Override
+        public void addQueueItem(MediaDescriptionCompat description) {
+            try {
+                long flags = mBinder.getFlags();
+                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+                    throw new UnsupportedOperationException(
+                            "This session doesn't support queue management operations");
+                }
+                mBinder.addQueueItem(description);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in addQueueItem.", e);
+            }
+        }
+
+        @Override
+        public void addQueueItem(MediaDescriptionCompat description, int index) {
+            try {
+                long flags = mBinder.getFlags();
+                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+                    throw new UnsupportedOperationException(
+                            "This session doesn't support queue management operations");
+                }
+                mBinder.addQueueItemAt(description, index);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in addQueueItemAt.", e);
+            }
+        }
+
+        @Override
+        public void removeQueueItem(MediaDescriptionCompat description) {
+            try {
+                long flags = mBinder.getFlags();
+                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+                    throw new UnsupportedOperationException(
+                            "This session doesn't support queue management operations");
+                }
+                mBinder.removeQueueItem(description);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in removeQueueItem.", e);
+            }
+        }
+
+        @Override
+        public void removeQueueItemAt(int index) {
+            try {
+                long flags = mBinder.getFlags();
+                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+                    throw new UnsupportedOperationException(
+                            "This session doesn't support queue management operations");
+                }
+                mBinder.removeQueueItemAt(index);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in removeQueueItemAt.", e);
+            }
+        }
+
+        @Override
         public CharSequence getQueueTitle() {
             try {
                 return mBinder.getQueueTitle();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getQueueTitle. " + e);
+                Log.e(TAG, "Dead object in getQueueTitle.", e);
             }
             return null;
         }
@@ -1083,7 +1298,7 @@
             try {
                 return mBinder.getExtras();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getExtras. " + e);
+                Log.e(TAG, "Dead object in getExtras.", e);
             }
             return null;
         }
@@ -1093,17 +1308,37 @@
             try {
                 return mBinder.getRatingType();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getRatingType. " + e);
+                Log.e(TAG, "Dead object in getRatingType.", e);
             }
             return 0;
         }
 
         @Override
+        public int getRepeatMode() {
+            try {
+                return mBinder.getRepeatMode();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in getRepeatMode.", e);
+            }
+            return 0;
+        }
+
+        @Override
+        public boolean isShuffleModeEnabled() {
+            try {
+                return mBinder.isShuffleModeEnabled();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in isShuffleModeEnabled.", e);
+            }
+            return false;
+        }
+
+        @Override
         public long getFlags() {
             try {
                 return mBinder.getFlags();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getFlags. " + e);
+                Log.e(TAG, "Dead object in getFlags.", e);
             }
             return 0;
         }
@@ -1116,7 +1351,7 @@
                         info.controlType, info.maxVolume, info.currentVolume);
                 return pi;
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getPlaybackInfo. " + e);
+                Log.e(TAG, "Dead object in getPlaybackInfo.", e);
             }
             return null;
         }
@@ -1126,7 +1361,7 @@
             try {
                 return mBinder.getLaunchPendingIntent();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getSessionActivity. " + e);
+                Log.e(TAG, "Dead object in getSessionActivity.", e);
             }
             return null;
         }
@@ -1136,7 +1371,7 @@
             try {
                 mBinder.setVolumeTo(value, flags, null);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in setVolumeTo. " + e);
+                Log.e(TAG, "Dead object in setVolumeTo.", e);
             }
         }
 
@@ -1145,7 +1380,7 @@
             try {
                 mBinder.adjustVolume(direction, flags, null);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in adjustVolume. " + e);
+                Log.e(TAG, "Dead object in adjustVolume.", e);
             }
         }
 
@@ -1155,7 +1390,7 @@
                 mBinder.sendCommand(command, params,
                         new MediaSessionCompat.ResultReceiverWrapper(cb));
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in sendCommand. " + e);
+                Log.e(TAG, "Dead object in sendCommand.", e);
             }
         }
 
@@ -1164,7 +1399,7 @@
             try {
                 return mBinder.getPackageName();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in getPackageName. " + e);
+                Log.e(TAG, "Dead object in getPackageName.", e);
             }
             return null;
         }
@@ -1187,7 +1422,7 @@
             try {
                 mBinder.prepare();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in prepare. " + e);
+                Log.e(TAG, "Dead object in prepare.", e);
             }
         }
 
@@ -1196,7 +1431,7 @@
             try {
                 mBinder.prepareFromMediaId(mediaId, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in prepareFromMediaId. " + e);
+                Log.e(TAG, "Dead object in prepareFromMediaId.", e);
             }
         }
 
@@ -1205,7 +1440,7 @@
             try {
                 mBinder.prepareFromSearch(query, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in prepareFromSearch. " + e);
+                Log.e(TAG, "Dead object in prepareFromSearch.", e);
             }
         }
 
@@ -1214,7 +1449,7 @@
             try {
                 mBinder.prepareFromUri(uri, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in prepareFromUri. " + e);
+                Log.e(TAG, "Dead object in prepareFromUri.", e);
             }
         }
 
@@ -1223,7 +1458,7 @@
             try {
                 mBinder.play();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in play. " + e);
+                Log.e(TAG, "Dead object in play.", e);
             }
         }
 
@@ -1232,7 +1467,7 @@
             try {
                 mBinder.playFromMediaId(mediaId, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in playFromMediaId. " + e);
+                Log.e(TAG, "Dead object in playFromMediaId.", e);
             }
         }
 
@@ -1241,7 +1476,7 @@
             try {
                 mBinder.playFromSearch(query, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in playFromSearch. " + e);
+                Log.e(TAG, "Dead object in playFromSearch.", e);
             }
         }
 
@@ -1250,7 +1485,7 @@
             try {
                 mBinder.playFromUri(uri, extras);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in playFromUri. " + e);
+                Log.e(TAG, "Dead object in playFromUri.", e);
             }
         }
 
@@ -1259,7 +1494,7 @@
             try {
                 mBinder.skipToQueueItem(id);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in skipToQueueItem. " + e);
+                Log.e(TAG, "Dead object in skipToQueueItem.", e);
             }
         }
 
@@ -1268,7 +1503,7 @@
             try {
                 mBinder.pause();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in pause. " + e);
+                Log.e(TAG, "Dead object in pause.", e);
             }
         }
 
@@ -1277,7 +1512,7 @@
             try {
                 mBinder.stop();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in stop. " + e);
+                Log.e(TAG, "Dead object in stop.", e);
             }
         }
 
@@ -1286,7 +1521,7 @@
             try {
                 mBinder.seekTo(pos);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in seekTo. " + e);
+                Log.e(TAG, "Dead object in seekTo.", e);
             }
         }
 
@@ -1295,7 +1530,7 @@
             try {
                 mBinder.fastForward();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in fastForward. " + e);
+                Log.e(TAG, "Dead object in fastForward.", e);
             }
         }
 
@@ -1304,7 +1539,7 @@
             try {
                 mBinder.next();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in skipToNext. " + e);
+                Log.e(TAG, "Dead object in skipToNext.", e);
             }
         }
 
@@ -1313,7 +1548,7 @@
             try {
                 mBinder.rewind();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in rewind. " + e);
+                Log.e(TAG, "Dead object in rewind.", e);
             }
         }
 
@@ -1322,7 +1557,7 @@
             try {
                 mBinder.previous();
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in skipToPrevious. " + e);
+                Log.e(TAG, "Dead object in skipToPrevious.", e);
             }
         }
 
@@ -1331,7 +1566,25 @@
             try {
                 mBinder.rate(rating);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in setRating. " + e);
+                Log.e(TAG, "Dead object in setRating.", e);
+            }
+        }
+
+        @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            try {
+                mBinder.setRepeatMode(repeatMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in setRepeatMode.", e);
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            try {
+                mBinder.setShuffleModeEnabled(enabled);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in setShuffleModeEnabled.", e);
             }
         }
 
@@ -1345,7 +1598,7 @@
             try {
                 mBinder.sendCustomAction(action, args);
             } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in sendCustomAction. " + e);
+                Log.e(TAG, "Dead object in sendCustomAction.", e);
             }
         }
     }
@@ -1357,7 +1610,7 @@
         // after API 21.
         private IMediaSession mExtraBinder;
         private HashMap<Callback, ExtraCallback> mCallbackMap = new HashMap<>();
-        private List<Callback> mPendingCallbacks;
+        private List<Callback> mPendingCallbacks = new ArrayList<>();
 
         public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
             mControllerObj = MediaControllerCompatApi21.fromToken(context,
@@ -1385,14 +1638,13 @@
                 try {
                     mExtraBinder.registerCallbackListener(extraCallback);
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Dead object in registerCallback. " + e);
+                    Log.e(TAG, "Dead object in registerCallback.", e);
                 }
             } else {
-                if (mPendingCallbacks == null) {
-                    mPendingCallbacks = new ArrayList<>();
-                }
                 callback.setHandler(handler);
-                mPendingCallbacks.add(callback);
+                synchronized (mPendingCallbacks) {
+                    mPendingCallbacks.add(callback);
+                }
             }
         }
 
@@ -1406,13 +1658,12 @@
                         mExtraBinder.unregisterCallbackListener(extraCallback);
                     }
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Dead object in unregisterCallback. " + e);
+                    Log.e(TAG, "Dead object in unregisterCallback.", e);
                 }
             } else {
-                if (mPendingCallbacks == null) {
-                    mPendingCallbacks = new ArrayList<>();
+                synchronized (mPendingCallbacks) {
+                    mPendingCallbacks.remove(callback);
                 }
-                mPendingCallbacks.remove(callback);
             }
         }
 
@@ -1429,11 +1680,11 @@
 
         @Override
         public PlaybackStateCompat getPlaybackState() {
-            if (android.os.Build.VERSION.SDK_INT < 22 && mExtraBinder != null) {
+            if (mExtraBinder != null) {
                 try {
                     return mExtraBinder.getPlaybackState();
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Dead object in getPlaybackState. " + e);
+                    Log.e(TAG, "Dead object in getPlaybackState.", e);
                 }
             }
             Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
@@ -1454,6 +1705,35 @@
         }
 
         @Override
+        public void addQueueItem(MediaDescriptionCompat description) {
+            Bundle params = new Bundle();
+            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+            sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
+        }
+
+        @Override
+        public void addQueueItem(MediaDescriptionCompat description, int index) {
+            Bundle params = new Bundle();
+            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+            params.putInt(COMMAND_ARGUMENT_INDEX, index);
+            sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null);
+        }
+
+        @Override
+        public void removeQueueItem(MediaDescriptionCompat description) {
+            Bundle params = new Bundle();
+            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+            sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null);
+        }
+
+        @Override
+        public void removeQueueItemAt(int index) {
+            Bundle params = new Bundle();
+            params.putInt(COMMAND_ARGUMENT_INDEX, index);
+            sendCommand(COMMAND_REMOVE_QUEUE_ITEM_AT, params, null);
+        }
+
+        @Override
         public CharSequence getQueueTitle() {
             return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
         }
@@ -1469,13 +1749,37 @@
                 try {
                     return mExtraBinder.getRatingType();
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Dead object in getRatingType. " + e);
+                    Log.e(TAG, "Dead object in getRatingType.", e);
                 }
             }
             return MediaControllerCompatApi21.getRatingType(mControllerObj);
         }
 
         @Override
+        public int getRepeatMode() {
+            if (mExtraBinder != null) {
+                try {
+                    return mExtraBinder.getRepeatMode();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Dead object in getRepeatMode.", e);
+                }
+            }
+            return PlaybackStateCompat.REPEAT_MODE_NONE;
+        }
+
+        @Override
+        public boolean isShuffleModeEnabled() {
+            if (mExtraBinder != null) {
+                try {
+                    return mExtraBinder.isShuffleModeEnabled();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Dead object in isShuffleModeEnabled.", e);
+                }
+            }
+            return false;
+        }
+
+        @Override
         public long getFlags() {
             return MediaControllerCompatApi21.getFlags(mControllerObj);
         }
@@ -1528,21 +1832,23 @@
         }
 
         private void processPendingCallbacks() {
-            if (mPendingCallbacks == null || mExtraBinder == null) {
+            if (mExtraBinder == null) {
                 return;
             }
-            for (Callback callback : mPendingCallbacks) {
-                ExtraCallback extraCallback = new ExtraCallback(callback);
-                mCallbackMap.put(callback, extraCallback);
-                callback.mHasExtraCallback = true;
-                try {
-                    mExtraBinder.registerCallbackListener(extraCallback);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Dead object in registerCallback. " + e);
-                    break;
+            synchronized (mPendingCallbacks) {
+                for (Callback callback : mPendingCallbacks) {
+                    ExtraCallback extraCallback = new ExtraCallback(callback);
+                    mCallbackMap.put(callback, extraCallback);
+                    callback.mHasExtraCallback = true;
+                    try {
+                        mExtraBinder.registerCallbackListener(extraCallback);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Dead object in registerCallback.", e);
+                        break;
+                    }
                 }
+                mPendingCallbacks.clear();
             }
-            mPendingCallbacks = null;
         }
 
         private static class ExtraBinderRequestResultReceiver extends ResultReceiver {
@@ -1619,6 +1925,26 @@
             }
 
             @Override
+            public void onRepeatModeChanged(final int repeatMode) throws RemoteException {
+                mCallback.mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onRepeatModeChanged(repeatMode);
+                    }
+                });
+            }
+
+            @Override
+            public void onShuffleModeChanged(final boolean enabled) throws RemoteException {
+                mCallback.mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onShuffleModeChanged(enabled);
+                    }
+                });
+            }
+
+            @Override
             public void onExtrasChanged(Bundle extras) throws RemoteException {
                 // Will not be called.
                 throw new AssertionError();
@@ -1715,6 +2041,20 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            Bundle bundle = new Bundle();
+            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
+            sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED, enabled);
+            sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE_ENABLED, bundle);
+        }
+
+        @Override
         public void playFromMediaId(String mediaId, Bundle extras) {
             MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
                     extras);
@@ -1833,5 +2173,4 @@
             MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
         }
     }
-
 }
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index 53e6c70..01ac807 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -94,7 +94,10 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    @IntDef(flag=true, value={FLAG_HANDLES_MEDIA_BUTTONS, FLAG_HANDLES_TRANSPORT_CONTROLS})
+    @IntDef(flag=true, value={
+            FLAG_HANDLES_MEDIA_BUTTONS,
+            FLAG_HANDLES_TRANSPORT_CONTROLS,
+            FLAG_HANDLES_QUEUE_COMMANDS })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SessionFlags {}
 
@@ -111,6 +114,12 @@
     public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
 
     /**
+     * Set this flag on the session to indicate that it handles queue
+     * management commands through its {@link Callback}.
+     */
+    public static final int FLAG_HANDLES_QUEUE_COMMANDS = 1 << 2;
+
+    /**
      * Custom action to invoke playFromUri() for the forward compatibility.
      */
     static final String ACTION_PLAY_FROM_URI =
@@ -140,6 +149,18 @@
             "android.support.v4.media.session.action.PREPARE_FROM_URI";
 
     /**
+     * Custom action to invoke setRepeatMode() for the forward compatibility.
+     */
+    static final String ACTION_SET_REPEAT_MODE =
+            "android.support.v4.media.session.action.SET_REPEAT_MODE";
+
+    /**
+     * Custom action to invoke setShuffleModeEnabled() for the forward compatibility.
+     */
+    static final String ACTION_SET_SHUFFLE_MODE_ENABLED =
+            "android.support.v4.media.session.action.SET_SHUFFLE_MODE_ENABLED";
+
+    /**
      * Argument for use with {@link #ACTION_PREPARE_FROM_MEDIA_ID} indicating media id to play.
      */
     static final String ACTION_ARGUMENT_MEDIA_ID =
@@ -164,6 +185,19 @@
     static final String ACTION_ARGUMENT_EXTRAS =
             "android.support.v4.media.session.action.ARGUMENT_EXTRAS";
 
+    /**
+     * Argument for use with {@link #ACTION_SET_REPEAT_MODE} indicating repeat mode.
+     */
+    static final String ACTION_ARGUMENT_REPEAT_MODE =
+            "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE";
+
+    /**
+     * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE_ENABLED} indicating that shuffle mode
+     * is enabled.
+     */
+    static final String ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED =
+            "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE_ENABLED";
+
     static final String EXTRA_BINDER = "android.support.v4.media.session.EXTRA_BINDER";
 
     // Maximum size of the bitmap in dp.
@@ -238,6 +272,8 @@
         if (android.os.Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaSessionImplApi21(context, tag);
             mImpl.setMediaButtonReceiver(mbrIntent);
+            // Set default callback to respond to controllers' extra binder requests.
+            setCallback(new Callback() {});
         } else {
             mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent);
         }
@@ -501,6 +537,33 @@
     }
 
     /**
+     * Set the repeat mode for this session.
+     * <p>
+     * Note that if this method is not called before, {@link MediaControllerCompat#getRepeatMode}
+     * will return {@link PlaybackStateCompat#REPEAT_MODE_NONE}.
+     *
+     * @param repeatMode The repeat mode. Must be one of the followings:
+     *            {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+     *            {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+     *            {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+     */
+    public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        mImpl.setRepeatMode(repeatMode);
+    }
+
+    /**
+     * Set the shuffle mode for this session.
+     * <p>
+     * Note that if this method is not called before,
+     * {@link MediaControllerCompat#isShuffleModeEnabled} will return {@code false}.
+     *
+     * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+     */
+    public void setShuffleModeEnabled(boolean enabled) {
+        mImpl.setShuffleModeEnabled(enabled);
+    }
+
+    /**
      * Set some extras that can be associated with the
      * {@link MediaSessionCompat}. No assumptions should be made as to how a
      * {@link MediaControllerCompat} will handle these extras. Keys should be
@@ -787,6 +850,33 @@
         }
 
         /**
+         * Override to handle the setting of the repeat mode.
+         * <p>
+         * You should call {@link #setRepeatMode} before end of this method in order to notify
+         * the change to the {@link MediaControllerCompat}, or
+         * {@link MediaControllerCompat#getRepeatMode} could return an invalid value.
+         *
+         * @param repeatMode The repeat mode which is one of followings:
+         *            {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+         *            {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+         *            {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+         */
+        public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+        }
+
+        /**
+         * Override to handle the setting of the shuffle mode.
+         * <p>
+         * You should call {@link #setShuffleModeEnabled} before the end of this method in order to
+         * notify the change to the {@link MediaControllerCompat}, or
+         * {@link MediaControllerCompat#isShuffleModeEnabled} could return an invalid value.
+         *
+         * @param enabled true when the shuffle mode is enabled, false otherwise.
+         */
+        public void onSetShuffleModeEnabled(boolean enabled) {
+        }
+
+        /**
          * Called when a {@link MediaControllerCompat} wants a
          * {@link PlaybackStateCompat.CustomAction} to be performed.
          *
@@ -798,6 +888,48 @@
         public void onCustomAction(String action, Bundle extras) {
         }
 
+        /**
+         * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem}
+         * with the given {@link MediaDescriptionCompat description} at the end of the play queue.
+         *
+         * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem}
+         *            to be inserted.
+         */
+        public void onAddQueueItem(MediaDescriptionCompat description) {
+        }
+
+        /**
+         * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem}
+         * with the given {@link MediaDescriptionCompat description} at the specified position
+         * in the play queue.
+         *
+         * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem}
+         *            to be inserted.
+         * @param index The index at which the created {@link QueueItem} is to be inserted.
+         */
+        public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+        }
+
+        /**
+         * Called when a {@link MediaControllerCompat} wants to remove the first occurrence of the
+         * specified {@link QueueItem} with the given {@link MediaDescriptionCompat description}
+         * in the play queue.
+         *
+         * @param description The {@link MediaDescriptionCompat} for denoting the {@link QueueItem}
+         *            to be removed.
+         */
+        public void onRemoveQueueItem(MediaDescriptionCompat description) {
+        }
+
+        /**
+         * Called when a {@link MediaControllerCompat} wants to remove a {@link QueueItem} at the
+         * specified position in the play queue.
+         *
+         * @param index The index of the element to be removed.
+         */
+        public void onRemoveQueueItemAt(int index) {
+        }
+
         private class StubApi21 implements MediaSessionCompatApi21.Callback {
 
             StubApi21() {
@@ -812,6 +944,25 @@
                         BundleCompat.putBinder(result, EXTRA_BINDER, impl.getExtraSessionBinder());
                         cb.send(0, result);
                     }
+                } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM)) {
+                    extras.setClassLoader(MediaDescriptionCompat.class.getClassLoader());
+                    Callback.this.onAddQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(
+                                    MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION));
+                } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM_AT)) {
+                    extras.setClassLoader(MediaDescriptionCompat.class.getClassLoader());
+                    Callback.this.onAddQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(
+                                    MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION),
+                            extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX));
+                } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM)) {
+                    extras.setClassLoader(MediaDescriptionCompat.class.getClassLoader());
+                    Callback.this.onRemoveQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(
+                                    MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION));
+                } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM_AT)) {
+                    Callback.this.onRemoveQueueItemAt(
+                            extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX));
                 } else {
                     Callback.this.onCommand(command, extras, cb);
                 }
@@ -902,6 +1053,12 @@
                     Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI);
                     Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS);
                     Callback.this.onPrepareFromUri(uri, bundle);
+                } else if (action.equals(ACTION_SET_REPEAT_MODE)) {
+                    int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE);
+                    Callback.this.onSetRepeatMode(repeatMode);
+                } else if (action.equals(ACTION_SET_SHUFFLE_MODE_ENABLED)) {
+                    boolean enabled = extras.getBoolean(ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED);
+                    Callback.this.onSetShuffleModeEnabled(enabled);
                 } else {
                     Callback.this.onCustomAction(action, extras);
                 }
@@ -1281,6 +1438,8 @@
         void setQueueTitle(CharSequence title);
 
         void setRatingType(@RatingCompat.Style int type);
+        void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+        void setShuffleModeEnabled(boolean enabled);
         void setExtras(Bundle extras);
 
         Object getMediaSession();
@@ -1320,6 +1479,8 @@
         List<QueueItem> mQueue;
         CharSequence mQueueTitle;
         @RatingCompat.Style int mRatingType;
+        @PlaybackStateCompat.RepeatMode int mRepeatMode;
+        boolean mShuffleModeEnabled;
         Bundle mExtras;
 
         int mVolumeType;
@@ -1415,10 +1576,22 @@
             postToHandler(what, null);
         }
 
+        void postToHandler(int what, int arg1) {
+            postToHandler(what, null, arg1);
+        }
+
         void postToHandler(int what, Object obj) {
             postToHandler(what, obj, null);
         }
 
+        void postToHandler(int what, Object obj, int arg1) {
+            synchronized (mLock) {
+                if (mHandler != null) {
+                    mHandler.post(what, obj, arg1);
+                }
+            }
+        }
+
         void postToHandler(int what, Object obj, Bundle extras) {
             synchronized (mLock) {
                 if (mHandler != null) {
@@ -1607,6 +1780,22 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            if (mRepeatMode != repeatMode) {
+                mRepeatMode = repeatMode;
+                sendRepeatMode(repeatMode);
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            if (mShuffleModeEnabled != enabled) {
+                mShuffleModeEnabled = enabled;
+                sendShuffleModeEnabled(enabled);
+            }
+        }
+
+        @Override
         public void setExtras(Bundle extras) {
             mExtras = extras;
             sendExtras(extras);
@@ -1825,6 +2014,30 @@
             mControllerCallbacks.finishBroadcast();
         }
 
+        private void sendRepeatMode(int repeatMode) {
+            int size = mControllerCallbacks.beginBroadcast();
+            for (int i = size - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+                try {
+                    cb.onRepeatModeChanged(repeatMode);
+                } catch (RemoteException e) {
+                }
+            }
+            mControllerCallbacks.finishBroadcast();
+        }
+
+        private void sendShuffleModeEnabled(boolean enabled) {
+            int size = mControllerCallbacks.beginBroadcast();
+            for (int i = size - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+                try {
+                    cb.onShuffleModeChanged(enabled);
+                } catch (RemoteException e) {
+                }
+            }
+            mControllerCallbacks.finishBroadcast();
+        }
+
         private void sendExtras(Bundle extras) {
             int size = mControllerCallbacks.beginBroadcast();
             for (int i = size - 1; i >= 0; i--) {
@@ -2021,6 +2234,16 @@
             }
 
             @Override
+            public void setRepeatMode(int repeatMode) throws RemoteException {
+                postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode);
+            }
+
+            @Override
+            public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+                postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE_ENABLED, enabled);
+            }
+
+            @Override
             public void sendCustomAction(String action, Bundle args)
                     throws RemoteException {
                 postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args);
@@ -2044,6 +2267,26 @@
             }
 
             @Override
+            public void addQueueItem(MediaDescriptionCompat description) {
+                postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM, description);
+            }
+
+            @Override
+            public void addQueueItemAt(MediaDescriptionCompat description, int index) {
+                postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM_AT, description, index);
+            }
+
+            @Override
+            public void removeQueueItem(MediaDescriptionCompat description) {
+                postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM, description);
+            }
+
+            @Override
+            public void removeQueueItemAt(int index) {
+                postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM_AT, index);
+            }
+
+            @Override
             public CharSequence getQueueTitle() {
                 return mQueueTitle;
             }
@@ -2062,6 +2305,17 @@
             }
 
             @Override
+            @PlaybackStateCompat.RepeatMode
+            public int getRepeatMode() {
+                return mRepeatMode;
+            }
+
+            @Override
+            public boolean isShuffleModeEnabled() {
+                return mShuffleModeEnabled;
+            }
+
+            @Override
             public boolean isTransportControlEnabled() {
                 return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0;
             }
@@ -2103,6 +2357,12 @@
             private static final int MSG_CUSTOM_ACTION = 20;
             private static final int MSG_MEDIA_BUTTON = 21;
             private static final int MSG_SET_VOLUME = 22;
+            private static final int MSG_SET_REPEAT_MODE = 23;
+            private static final int MSG_SET_SHUFFLE_MODE_ENABLED = 24;
+            private static final int MSG_ADD_QUEUE_ITEM = 25;
+            private static final int MSG_ADD_QUEUE_ITEM_AT = 26;
+            private static final int MSG_REMOVE_QUEUE_ITEM = 27;
+            private static final int MSG_REMOVE_QUEUE_ITEM_AT = 28;
 
             // KeyEvent constants only available on API 11+
             private static final int KEYCODE_MEDIA_PAUSE = 127;
@@ -2204,11 +2464,29 @@
                     case MSG_CUSTOM_ACTION:
                         cb.onCustomAction((String) msg.obj, msg.getData());
                         break;
+                    case MSG_ADD_QUEUE_ITEM:
+                        cb.onAddQueueItem((MediaDescriptionCompat) msg.obj);
+                        break;
+                    case MSG_ADD_QUEUE_ITEM_AT:
+                        cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1);
+                        break;
+                    case MSG_REMOVE_QUEUE_ITEM:
+                        cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj);
+                        break;
+                    case MSG_REMOVE_QUEUE_ITEM_AT:
+                        cb.onRemoveQueueItemAt(msg.arg1);
+                        break;
                     case MSG_ADJUST_VOLUME:
-                        adjustVolume((int) msg.obj, 0);
+                        adjustVolume(msg.arg1, 0);
                         break;
                     case MSG_SET_VOLUME:
-                        setVolumeTo((int) msg.obj, 0);
+                        setVolumeTo(msg.arg1, 0);
+                        break;
+                    case MSG_SET_REPEAT_MODE:
+                        cb.onSetRepeatMode(msg.arg1);
+                        break;
+                    case MSG_SET_SHUFFLE_MODE_ENABLED:
+                        cb.onSetShuffleModeEnabled((boolean) msg.obj);
                         break;
                 }
             }
@@ -2286,6 +2564,8 @@
 
         private PlaybackStateCompat mPlaybackState;
         @RatingCompat.Style int mRatingType;
+        @PlaybackStateCompat.RepeatMode int mRepeatMode;
+        boolean mShuffleModeEnabled;
 
         public MediaSessionImplApi21(Context context, String tag) {
             mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
@@ -2361,18 +2641,16 @@
 
         @Override
         public void setPlaybackState(PlaybackStateCompat state) {
-            if (android.os.Build.VERSION.SDK_INT < 22) {
-                mPlaybackState = state;
-                int size = mExtraControllerCallbacks.beginBroadcast();
-                for (int i = size - 1; i >= 0; i--) {
-                    IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
-                    try {
-                        cb.onPlaybackStateChanged(state);
-                    } catch (RemoteException e) {
-                    }
+            mPlaybackState = state;
+            int size = mExtraControllerCallbacks.beginBroadcast();
+            for (int i = size - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+                try {
+                    cb.onPlaybackStateChanged(state);
+                } catch (RemoteException e) {
                 }
-                mExtraControllerCallbacks.finishBroadcast();
             }
+            mExtraControllerCallbacks.finishBroadcast();
             MediaSessionCompatApi21.setPlaybackState(mSessionObj,
                     state == null ? null : state.getPlaybackState());
         }
@@ -2420,6 +2698,38 @@
         }
 
         @Override
+        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+            if (mRepeatMode != repeatMode) {
+                mRepeatMode = repeatMode;
+                int size = mExtraControllerCallbacks.beginBroadcast();
+                for (int i = size - 1; i >= 0; i--) {
+                    IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+                    try {
+                        cb.onRepeatModeChanged(repeatMode);
+                    } catch (RemoteException e) {
+                    }
+                }
+                mExtraControllerCallbacks.finishBroadcast();
+            }
+        }
+
+        @Override
+        public void setShuffleModeEnabled(boolean enabled) {
+            if (mShuffleModeEnabled != enabled) {
+                mShuffleModeEnabled = enabled;
+                int size = mExtraControllerCallbacks.beginBroadcast();
+                for (int i = size - 1; i >= 0; i--) {
+                    IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+                    try {
+                        cb.onShuffleModeChanged(enabled);
+                    } catch (RemoteException e) {
+                    }
+                }
+                mExtraControllerCallbacks.finishBroadcast();
+            }
+        }
+
+        @Override
         public void setExtras(Bundle extras) {
             MediaSessionCompatApi21.setExtras(mSessionObj, extras);
         }
@@ -2621,6 +2931,18 @@
             }
 
             @Override
+            public void setRepeatMode(int repeatMode) throws RemoteException {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
+            public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
             public void sendCustomAction(String action, Bundle args) throws RemoteException {
                 // Will not be called.
                 throw new AssertionError();
@@ -2644,6 +2966,30 @@
             }
 
             @Override
+            public void addQueueItem(MediaDescriptionCompat descriptionCompat) {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
+            public void addQueueItemAt(MediaDescriptionCompat descriptionCompat, int index) {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
+            public void removeQueueItem(MediaDescriptionCompat description) {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
+            public void removeQueueItemAt(int index) {
+                // Will not be called.
+                throw new AssertionError();
+            }
+
+            @Override
             public CharSequence getQueueTitle() {
                 // Will not be called.
                 throw new AssertionError();
@@ -2662,6 +3008,17 @@
             }
 
             @Override
+            @PlaybackStateCompat.RepeatMode
+            public int getRepeatMode() {
+                return mRepeatMode;
+            }
+
+            @Override
+            public boolean isShuffleModeEnabled() {
+                return mShuffleModeEnabled;
+            }
+
+            @Override
             public boolean isTransportControlEnabled() {
                 // Will not be called.
                 throw new AssertionError();
diff --git a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
index be6e4b1..7b1ad31 100644
--- a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -49,7 +49,8 @@
             ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
             ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
             ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
-            ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI})
+            ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI,
+            ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE_ENABLED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface Actions {}
 
@@ -189,6 +190,20 @@
     public static final long ACTION_PREPARE_FROM_URI = 1 << 17;
 
     /**
+     * Indicates this session supports the set repeat mode command.
+     *
+     * @see Builder#setActions(long)
+     */
+    public static final long ACTION_SET_REPEAT_MODE = 1 << 18;
+
+    /**
+     * Indicates this session supports the set shuffle mode enabled command.
+     *
+     * @see Builder#setActions(long)
+     */
+    public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 1 << 19;
+
+    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -251,9 +266,10 @@
 
     /**
      * State indicating this item is currently in an error state. The error
-     * message should also be set when entering this state.
+     * code should also be set when entering this state.
      *
      * @see Builder#setState
+     * @see Builder#setErrorMessage(int, CharSequence)
      */
     public final static int STATE_ERROR = 7;
 
@@ -300,6 +316,116 @@
      */
     public final static long PLAYBACK_POSITION_UNKNOWN = -1;
 
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
+            ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+            ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+            ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING,
+            ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCode {}
+
+    /**
+     * This is the default error code and indicates that none of the other error codes applies.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_UNKNOWN_ERROR = 0;
+
+    /**
+     * Error code when the application state is invalid to fulfill the request.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_APP_ERROR = 1;
+
+    /**
+     * Error code when the request is not supported by the application.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_NOT_SUPPORTED = 2;
+
+    /**
+     * Error code when the request cannot be performed because authentication has expired.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;
+
+    /**
+     * Error code when a premium account is required for the request to succeed.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;
+
+    /**
+     * Error code when too many concurrent streams are detected.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;
+
+    /**
+     * Error code when the content is blocked due to parental controls.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;
+
+    /**
+     * Error code when the content is blocked due to being regionally unavailable.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;
+
+    /**
+     * Error code when the requested content is already playing.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;
+
+    /**
+     * Error code when the application cannot skip any more songs because skip limit is reached.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;
+
+    /**
+     * Error code when the action is interrupted due to some external event.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_ACTION_ABORTED = 10;
+
+    /**
+     * Error code when the playback navigation (previous, next) is not possible because the queue
+     * was exhausted.
+     * The error code should be set when entering {@link #STATE_ERROR}.
+     */
+    public static final int ERROR_CODE_END_OF_QUEUE = 11;
+
+    /**
+     * @hide
+     */
+    @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RepeatMode {}
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback will be stopped at the end of the playing media list.
+     */
+    public static final int REPEAT_MODE_NONE = 0;
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback of the current playing media item will be repeated.
+     */
+    public static final int REPEAT_MODE_ONE = 1;
+
+    /**
+     * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+     * to indicate that the playback of the playing media list will be repeated.
+     */
+    public static final int REPEAT_MODE_ALL = 2;
+
     // KeyEvent constants only available on API 11+
     private static final int KEYCODE_MEDIA_PAUSE = 127;
     private static final int KEYCODE_MEDIA_PLAY = 126;
@@ -348,6 +474,7 @@
     final long mBufferedPosition;
     final float mSpeed;
     final long mActions;
+    final int mErrorCode;
     final CharSequence mErrorMessage;
     final long mUpdateTime;
     List<PlaybackStateCompat.CustomAction> mCustomActions;
@@ -357,7 +484,7 @@
     private Object mStateObj;
 
     PlaybackStateCompat(int state, long position, long bufferedPosition,
-            float rate, long actions, CharSequence errorMessage, long updateTime,
+            float rate, long actions, int errorCode, CharSequence errorMessage, long updateTime,
             List<PlaybackStateCompat.CustomAction> customActions,
             long activeItemId, Bundle extras) {
         mState = state;
@@ -365,6 +492,7 @@
         mBufferedPosition = bufferedPosition;
         mSpeed = rate;
         mActions = actions;
+        mErrorCode = errorCode;
         mErrorMessage = errorMessage;
         mUpdateTime = updateTime;
         mCustomActions = new ArrayList<>(customActions);
@@ -383,6 +511,8 @@
         mCustomActions = in.createTypedArrayList(CustomAction.CREATOR);
         mActiveItemId = in.readLong();
         mExtras = in.readBundle();
+        // New attributes should be added at the end for backward compatibility.
+        mErrorCode = in.readInt();
     }
 
     @Override
@@ -394,7 +524,8 @@
         bob.append(", speed=").append(mSpeed);
         bob.append(", updated=").append(mUpdateTime);
         bob.append(", actions=").append(mActions);
-        bob.append(", error=").append(mErrorMessage);
+        bob.append(", error code=").append(mErrorCode);
+        bob.append(", error message=").append(mErrorMessage);
         bob.append(", custom actions=").append(mCustomActions);
         bob.append(", active item id=").append(mActiveItemId);
         bob.append("}");
@@ -418,6 +549,8 @@
         dest.writeTypedList(mCustomActions);
         dest.writeLong(mActiveItemId);
         dest.writeBundle(mExtras);
+        // New attributes should be added at the end for backward compatibility.
+        dest.writeInt(mErrorCode);
     }
 
     /**
@@ -490,6 +623,8 @@
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
      * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+     * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+     * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
      * </ul>
      */
     @Actions
@@ -505,8 +640,33 @@
     }
 
     /**
-     * Get a user readable error message. This should be set when the state is
+     * Get the error code. This should be set when the state is
      * {@link PlaybackStateCompat#STATE_ERROR}.
+     *
+     * @see #ERROR_CODE_UNKNOWN_ERROR
+     * @see #ERROR_CODE_APP_ERROR
+     * @see #ERROR_CODE_NOT_SUPPORTED
+     * @see #ERROR_CODE_AUTHENTICATION_EXPIRED
+     * @see #ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED
+     * @see #ERROR_CODE_CONCURRENT_STREAM_LIMIT
+     * @see #ERROR_CODE_PARENTAL_CONTROL_RESTRICTED
+     * @see #ERROR_CODE_NOT_AVAILABLE_IN_REGION
+     * @see #ERROR_CODE_CONTENT_ALREADY_PLAYING
+     * @see #ERROR_CODE_SKIP_LIMIT_REACHED
+     * @see #ERROR_CODE_ACTION_ABORTED
+     * @see #ERROR_CODE_END_OF_QUEUE
+     * @see #getErrorMessage()
+     */
+    @ErrorCode
+    public int getErrorCode() {
+        return mErrorCode;
+    }
+
+    /**
+     * Get the user readable optional error message. This may be set when the state is
+     * {@link PlaybackStateCompat#STATE_ERROR}.
+     *
+     * @see #getErrorCode()
      */
     public CharSequence getErrorMessage() {
         return mErrorMessage;
@@ -574,6 +734,7 @@
                 PlaybackStateCompatApi21.getBufferedPosition(stateObj),
                 PlaybackStateCompatApi21.getPlaybackSpeed(stateObj),
                 PlaybackStateCompatApi21.getActions(stateObj),
+                ERROR_CODE_UNKNOWN_ERROR,
                 PlaybackStateCompatApi21.getErrorMessage(stateObj),
                 PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj),
                 customActions,
@@ -856,6 +1017,7 @@
         private long mBufferedPosition;
         private float mRate;
         private long mActions;
+        private int mErrorCode;
         private CharSequence mErrorMessage;
         private long mUpdateTime;
         private long mActiveItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
@@ -880,6 +1042,7 @@
             mUpdateTime = source.mUpdateTime;
             mBufferedPosition = source.mBufferedPosition;
             mActions = source.mActions;
+            mErrorCode = source.mErrorCode;
             mErrorMessage = source.mErrorMessage;
             if (source.mCustomActions != null) {
                 mCustomActions.addAll(source.mCustomActions);
@@ -1000,6 +1163,8 @@
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
          * <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+         * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+         * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
          * </ul>
          *
          * @return this
@@ -1070,6 +1235,7 @@
          * is {@link PlaybackStateCompat#STATE_ERROR}.
          *
          * @return this
+         * @deprecated Use {@link #setErrorMessage(int, CharSequence)} instead.
          */
         public Builder setErrorMessage(CharSequence errorMessage) {
             mErrorMessage = errorMessage;
@@ -1077,6 +1243,20 @@
         }
 
         /**
+         * Set the error code with an optional user readable error message. This should be set when
+         * the state is {@link PlaybackStateCompat#STATE_ERROR}.
+         *
+         * @param errorCode The errorCode to set.
+         * @param errorMessage The user readable error message. Can be null.
+         * @return this
+         */
+        public Builder setErrorMessage(@ErrorCode int errorCode, CharSequence errorMessage) {
+            mErrorCode = errorCode;
+            mErrorMessage = errorMessage;
+            return this;
+        }
+
+        /**
          * Set any custom extras to be included with the playback state.
          *
          * @param extras The extras to include.
@@ -1092,7 +1272,7 @@
          */
         public PlaybackStateCompat build() {
             return new PlaybackStateCompat(mState, mPosition, mBufferedPosition,
-                    mRate, mActions, mErrorMessage, mUpdateTime,
+                    mRate, mActions, mErrorCode, mErrorMessage, mUpdateTime,
                     mCustomActions, mActiveItemId, mExtras);
         }
     }
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index 1216194..93ead1e 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -26,12 +26,16 @@
 
     <application android:supportsRtl="true">
         <uses-library android:name="android.test.runner"/>
-        <activity android:name="android.support.v4.media.session.TestActivity" />
         <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
             <intent-filter>
                 <action android:name="android.intent.action.MEDIA_BUTTON" />
             </intent-filter>
         </receiver>
+        <service android:name="android.support.v4.media.StubMediaBrowserServiceCompat">
+            <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
new file mode 100644
index 0000000..fd87a76
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserCompatTest {
+
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+
+    /**
+     * To check {@link MediaBrowserCompat#unsubscribe} works properly,
+     * we notify to the browser after the unsubscription that the media items have changed.
+     * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
+     *
+     * The measured time from calling {@link StubMediaBrowserServiceCompat#notifyChildrenChanged}
+     * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
+     * 50ms.
+     * So we make the thread sleep for 100ms to properly check that the callback is not called.
+     */
+    private static final long SLEEP_MS = 100L;
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.test",
+            "android.support.v4.media.StubMediaBrowserServiceCompat");
+    private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
+            "invalid.package", "invalid.ServiceClassName");
+    private final StubConnectionCallback mConnectionCallback = new StubConnectionCallback();
+    private final StubSubscriptionCallback mSubscriptionCallback = new StubSubscriptionCallback();
+    private final StubItemCallback mItemCallback = new StubItemCallback();
+
+    private MediaBrowserCompat mMediaBrowser;
+
+    @Test
+    @SmallTest
+    public void testMediaBrowser() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        assertEquals(false, mMediaBrowser.isConnected());
+
+        connectMediaBrowserService();
+        assertEquals(true, mMediaBrowser.isConnected());
+
+        assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+        assertEquals(StubMediaBrowserServiceCompat.EXTRAS_VALUE,
+                mMediaBrowser.getExtras().getString(StubMediaBrowserServiceCompat.EXTRAS_KEY));
+        assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
+                mMediaBrowser.getSessionToken());
+
+        mMediaBrowser.disconnect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return !mMediaBrowser.isConnected();
+            }
+        }.run();
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectTwice() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        try {
+            mMediaBrowser.connect();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectionFailed() {
+        resetCallbacks();
+        createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
+
+        mMediaBrowser.connect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mConnectionCallback.mConnectionFailedCount > 0
+                        && mConnectionCallback.mConnectedCount == 0
+                        && mConnectionCallback.mConnectionSuspendedCount == 0;
+            }
+        }.run();
+    }
+
+    @Test
+    @SmallTest
+    public void testGetServiceComponentBeforeConnection() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        try {
+            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribe() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mSubscriptionCallback.mChildrenLoadedCount > 0;
+            }
+        }.run();
+
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
+                mSubscriptionCallback.mLastParentId);
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length,
+                mSubscriptionCallback.mLastChildMediaItems.size());
+        for (int i = 0; i < StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length; ++i) {
+            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[i],
+                    mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+        }
+
+        // Test unsubscribe.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
+                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeWithOptions() {
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        final int pageSize = 3;
+        final int lastPage =
+                (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) / pageSize;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+        for (int page = 0; page <= lastPage; ++page) {
+            resetCallbacks();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
+                    mSubscriptionCallback);
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                protected boolean check() {
+                    return mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0;
+                }
+            }.run();
+            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
+                    mSubscriptionCallback.mLastParentId);
+            if (page != lastPage) {
+                assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
+            } else {
+                assertEquals(
+                        (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
+                        mSubscriptionCallback.mLastChildMediaItems.size());
+            }
+            // Check whether all the items in the current page are loaded.
+            for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
+                assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[page * pageSize + i],
+                        mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+            }
+        }
+
+        // Test unsubscribe with callback argument.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
+                mSubscriptionCallback);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
+                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItem() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
+                mSubscriptionCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mSubscriptionCallback.mLastErrorId != null;
+            }
+        }.run();
+
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
+                mSubscriptionCallback.mLastErrorId);
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItemWithOptions() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+
+        final int pageSize = 5;
+        final int page = 2;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+        mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, options,
+                mSubscriptionCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mSubscriptionCallback.mLastErrorId != null;
+            }
+        }.run();
+
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
+                mSubscriptionCallback.mLastErrorId);
+        assertEquals(page,
+                mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
+        assertEquals(pageSize,
+                mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnsubscribeForMultipleSubscriptions() {
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options, callback);
+
+            // Each onChildrenLoaded() must be called.
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                protected boolean check() {
+                    return callback.mChildrenLoadedWithOptionCount == 1;
+                }
+            }.run();
+        }
+
+        // Reset callbacks and unsubscribe.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            callback.reset();
+        }
+        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
+                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+
+        // onChildrenLoaded should not be called.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            assertEquals(0, callback.mChildrenLoadedWithOptionCount);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() {
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options, callback);
+
+            // Each onChildrenLoaded() must be called.
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                protected boolean check() {
+                    return callback.mChildrenLoadedWithOptionCount == 1;
+                }
+            }.run();
+        }
+
+        // Unsubscribe existing subscriptions one-by-one.
+        final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
+        for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
+            // Reset callbacks
+            for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+                callback.reset();
+            }
+
+            // Remove one subscription
+            mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
+                    subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
+
+            // Make StubMediaBrowserServiceCompat notify that the children are changed.
+            StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
+                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+            try {
+                Thread.sleep(SLEEP_MS);
+            } catch (InterruptedException e) {
+                fail("Unexpected InterruptedException occurred.");
+            }
+
+            // Only the remaining subscriptionCallbacks should be called.
+            for (int j = 0; j < 4; j++) {
+                int childrenLoadedWithOptionsCount = subscriptionCallbacks
+                        .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
+                if (j <= i) {
+                    assertEquals(0, childrenLoadedWithOptionsCount);
+                } else {
+                    assertEquals(1, childrenLoadedWithOptionsCount);
+                }
+            }
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItem() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0], mItemCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mItemCallback.mLastMediaItem != null;
+            }
+        }.run();
+
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
+                mItemCallback.mLastMediaItem.getMediaId());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenOnLoadItemIsNotImplemented() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
+                mItemCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mItemCallback.mLastErrorId != null;
+            }
+        }.run();
+
+        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
+                mItemCallback.mLastErrorId);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenMediaIdIsInvalid() {
+        resetCallbacks();
+        mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
+
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        connectMediaBrowserService();
+        mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, mItemCallback);
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                // MediaBrowserServiceCompat.onLoadItem implementations must send null result when
+                // the given media id is invalid.
+                return mItemCallback.mLastMediaItem == null;
+            }
+        }.run();
+
+        assertNull(mItemCallback.mLastErrorId);
+    }
+
+    private void createMediaBrowser(final ComponentName component) {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        component, mConnectionCallback, null);
+            }
+        });
+    }
+
+    private void connectMediaBrowserService() {
+        mMediaBrowser.connect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mConnectionCallback.mConnectedCount > 0;
+            }
+        }.run();
+    }
+
+    private void resetCallbacks() {
+        mConnectionCallback.reset();
+        mSubscriptionCallback.reset();
+        mItemCallback.reset();
+    }
+
+    private static class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        volatile int mConnectedCount;
+        volatile int mConnectionFailedCount;
+        volatile int mConnectionSuspendedCount;
+
+        public void reset() {
+            mConnectedCount = 0;
+            mConnectionFailedCount = 0;
+            mConnectionSuspendedCount = 0;
+        }
+
+        @Override
+        public void onConnected() {
+            mConnectedCount++;
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            mConnectionFailedCount++;
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            mConnectionSuspendedCount++;
+        }
+    }
+
+    private static class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+        private volatile int mChildrenLoadedCount;
+        private volatile int mChildrenLoadedWithOptionCount;
+        private volatile String mLastErrorId;
+        private volatile String mLastParentId;
+        private volatile Bundle mLastOptions;
+        private volatile List<MediaItem> mLastChildMediaItems;
+
+        public void reset() {
+            mChildrenLoadedCount = 0;
+            mChildrenLoadedWithOptionCount = 0;
+            mLastErrorId = null;
+            mLastParentId = null;
+            mLastOptions = null;
+            mLastChildMediaItems = null;
+        }
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+            mChildrenLoadedCount++;
+            mLastParentId = parentId;
+            mLastChildMediaItems = children;
+        }
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
+            mChildrenLoadedWithOptionCount++;
+            mLastParentId = parentId;
+            mLastOptions = options;
+            mLastChildMediaItems = children;
+        }
+
+        @Override
+        public void onError(String id) {
+            mLastErrorId = id;
+        }
+
+        @Override
+        public void onError(String id, Bundle options) {
+            mLastErrorId = id;
+            mLastOptions = options;
+        }
+    }
+
+    private static class StubItemCallback extends MediaBrowserCompat.ItemCallback {
+        private volatile MediaItem mLastMediaItem;
+        private volatile String mLastErrorId;
+
+        public void reset() {
+            mLastMediaItem = null;
+            mLastErrorId = null;
+        }
+
+        @Override
+        public void onItemLoaded(MediaItem item) {
+            mLastMediaItem = item;
+        }
+
+        @Override
+        public void onError(String id) {
+            mLastErrorId = id;
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
new file mode 100644
index 0000000..7e436ec
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.media;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserServiceCompatTest {
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.test",
+            "android.support.v4.media.StubMediaBrowserServiceCompat");
+    private final Object mWaitLock = new Object();
+
+    private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
+    private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
+    private final ItemCallback mItemCallback = new ItemCallback();
+    private final SearchCallback mSearchCallback = new SearchCallback();
+
+    private MediaBrowserCompat mMediaBrowser;
+    private StubMediaBrowserServiceCompat mMediaBrowserService;
+    private Bundle mRootHints;
+
+    @Before
+    public void setUp() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mRootHints = new Bundle();
+                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
+                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
+                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+            }
+        });
+        synchronized (mWaitLock) {
+            mMediaBrowser.connect();
+            mWaitLock.wait(TIME_OUT_MS);
+        }
+        assertNotNull(mMediaBrowserService);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetSessionToken() {
+        assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
+                mMediaBrowserService.getSessionToken());
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyChildrenChanged() throws Exception {
+        synchronized (mWaitLock) {
+            mSubscriptionCallback.reset();
+            mMediaBrowser.subscribe(
+                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
+
+            mSubscriptionCallback.reset();
+            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyChildrenChangedWithPagination() throws Exception {
+        synchronized (mWaitLock) {
+            final int pageSize = 5;
+            final int page = 2;
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+
+            mSubscriptionCallback.reset();
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
+                    mSubscriptionCallback);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
+
+            mSubscriptionCallback.reset();
+            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
+        }
+    }
+
+    @Test
+    @LargeTest
+    public void testDelayedNotifyChildrenChanged() throws Exception {
+        synchronized (mWaitLock) {
+            mSubscriptionCallback.reset();
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED,
+                    mSubscriptionCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
+
+            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
+
+            mSubscriptionCallback.reset();
+            mMediaBrowserService.notifyChildrenChanged(
+                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
+
+            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
+        }
+    }
+
+    // TODO(hdmoon): Uncomment after fixing failing tests. (Fails on API >= 24)
+    // @Test
+    // @SmallTest
+    public void testDelayedItem() throws Exception {
+        synchronized (mWaitLock) {
+            mItemCallback.reset();
+            mMediaBrowser.getItem(
+                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertFalse(mItemCallback.mOnItemLoaded);
+
+            mItemCallback.reset();
+            mMediaBrowserService.sendDelayedItemLoaded();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mItemCallback.mOnItemLoaded);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSearch() throws Exception {
+        final String key = "test-key";
+        final String val = "test-val";
+
+        synchronized (mWaitLock) {
+            mSearchCallback.reset();
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertTrue(mSearchCallback.mSearchResults != null
+                    && mSearchCallback.mSearchResults.size() == 0);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNull(mSearchCallback.mSearchResults);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            Bundle extras = new Bundle();
+            extras.putString(key, val);
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNotNull(mSearchCallback.mSearchResults);
+            for (MediaItem item : mSearchCallback.mSearchResults) {
+                assertNotNull(item.getMediaId());
+                assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
+            }
+            assertNotNull(mSearchCallback.mSearchExtras);
+            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testBrowserRoot() {
+        final String id = "test-id";
+        final String key = "test-key";
+        final String val = "test-val";
+        final Bundle extras = new Bundle();
+        extras.putString(key, val);
+
+        MediaBrowserServiceCompat.BrowserRoot browserRoot =
+                new MediaBrowserServiceCompat.BrowserRoot(id, extras);
+        assertEquals(id, browserRoot.getRootId());
+        assertEquals(val, browserRoot.getExtras().getString(key));
+    }
+
+    private void assertRootHints(MediaItem item) {
+        Bundle rootHints = item.getDescription().getExtras();
+        assertNotNull(rootHints);
+        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
+                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT));
+        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE),
+                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE));
+        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED),
+                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED));
+    }
+
+    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mMediaBrowserService = StubMediaBrowserServiceCompat.sInstance;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class SubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+        boolean mOnChildrenLoaded;
+        boolean mOnChildrenLoadedWithOptions;
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+            synchronized (mWaitLock) {
+                mOnChildrenLoaded = true;
+                if (children != null) {
+                    for (MediaItem item : children) {
+                        assertRootHints(item);
+                    }
+                }
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
+            synchronized (mWaitLock) {
+                mOnChildrenLoadedWithOptions = true;
+                if (children != null) {
+                    for (MediaItem item : children) {
+                        assertRootHints(item);
+                    }
+                }
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnChildrenLoaded = false;
+            mOnChildrenLoadedWithOptions = false;
+        }
+    }
+
+    private class ItemCallback extends MediaBrowserCompat.ItemCallback {
+        boolean mOnItemLoaded;
+
+        @Override
+        public void onItemLoaded(MediaItem item) {
+            synchronized (mWaitLock) {
+                mOnItemLoaded = true;
+                assertRootHints(item);
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnItemLoaded = false;
+        }
+    }
+
+    private class SearchCallback extends MediaBrowserCompat.SearchCallback {
+        boolean mOnSearchResult;
+        Bundle mSearchExtras;
+        List<MediaItem> mSearchResults;
+
+        @Override
+        public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = items;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = null;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnSearchResult = false;
+            mSearchExtras = null;
+            mSearchResults = null;
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/MediaItemTest.java b/media-compat/tests/src/android/support/v4/media/MediaItemTest.java
new file mode 100644
index 0000000..bd2565f
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/MediaItemTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import org.junit.Test;
+
+/**
+ * Test {@link MediaBrowserCompat.MediaItem}.
+ */
+public class MediaItemTest {
+    private static final String DESCRIPTION = "test_description";
+    private static final String MEDIA_ID = "test_media_id";
+    private static final String TITLE = "test_title";
+    private static final String SUBTITLE = "test_subtitle";
+
+    @Test
+    @SmallTest
+    public void testBrowsableMediaItem() {
+        MediaDescriptionCompat description =
+                new MediaDescriptionCompat.Builder()
+                        .setDescription(DESCRIPTION)
+                        .setMediaId(MEDIA_ID)
+                        .setTitle(TITLE)
+                        .setSubtitle(SUBTITLE)
+                        .build();
+        MediaItem mediaItem = new MediaItem(description, MediaItem.FLAG_BROWSABLE);
+
+        assertEquals(description.toString(), mediaItem.getDescription().toString());
+        assertEquals(MEDIA_ID, mediaItem.getMediaId());
+        assertEquals(MediaItem.FLAG_BROWSABLE, mediaItem.getFlags());
+        assertTrue(mediaItem.isBrowsable());
+        assertFalse(mediaItem.isPlayable());
+        assertEquals(0, mediaItem.describeContents());
+
+        // Test writeToParcel
+        Parcel p = Parcel.obtain();
+        mediaItem.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        assertEquals(mediaItem.getFlags(), p.readInt());
+        assertEquals(
+                description.toString(),
+                MediaDescriptionCompat.CREATOR.createFromParcel(p).toString());
+        p.recycle();
+    }
+
+    @Test
+    @SmallTest
+    public void testPlayableMediaItem() {
+        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
+                .setDescription(DESCRIPTION)
+                .setMediaId(MEDIA_ID)
+                .setTitle(TITLE)
+                .setSubtitle(SUBTITLE)
+                .build();
+        MediaItem mediaItem = new MediaItem(description, MediaItem.FLAG_PLAYABLE);
+
+        assertEquals(description.toString(), mediaItem.getDescription().toString());
+        assertEquals(MEDIA_ID, mediaItem.getMediaId());
+        assertEquals(MediaItem.FLAG_PLAYABLE, mediaItem.getFlags());
+        assertFalse(mediaItem.isBrowsable());
+        assertTrue(mediaItem.isPlayable());
+        assertEquals(0, mediaItem.describeContents());
+
+        // Test writeToParcel
+        Parcel p = Parcel.obtain();
+        mediaItem.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        assertEquals(mediaItem.getFlags(), p.readInt());
+        assertEquals(
+                description.toString(),
+                MediaDescriptionCompat.CREATOR.createFromParcel(p).toString());
+        p.recycle();
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/PollingCheck.java b/media-compat/tests/src/android/support/v4/media/PollingCheck.java
new file mode 100644
index 0000000..222753f
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/PollingCheck.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+public abstract class PollingCheck {
+    private static final long TIME_SLICE = 50;
+    private long mTimeout = 3000;
+
+    public interface PollingCheckCondition {
+        boolean canProceed();
+    }
+
+    public PollingCheck() {
+    }
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        while (timeout > 0) {
+            if (condition.call()) {
+                return;
+            }
+
+            Thread.sleep(TIME_SLICE);
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail(message.toString());
+    }
+
+    public static void waitFor(final PollingCheckCondition condition) {
+        new PollingCheck() {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+
+    public static void waitFor(long timeout, final PollingCheckCondition condition) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
new file mode 100644
index 0000000..b943594
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
+    static final String EXTRAS_KEY = "test_extras_key";
+    static final String EXTRAS_VALUE = "test_extras_value";
+
+    static final String MEDIA_ID_INVALID = "test_media_id_invalid";
+    static final String MEDIA_ID_ROOT = "test_media_id_root";
+    static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed";
+    static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED =
+            "test_media_id_on_load_item_not_implemented";
+
+    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",
+            MEDIA_ID_CHILDREN_DELAYED
+    };
+
+    static final String SEARCH_QUERY = "test_media_children";
+    static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
+    static final String SEARCH_QUERY_FOR_ERROR = "query for error";
+
+    static StubMediaBrowserServiceCompat sInstance;
+
+    /* package private */ static MediaSessionCompat sSession;
+    private Bundle mExtras;
+    private Result<List<MediaItem>> mPendingLoadChildrenResult;
+    private Result<MediaItem> mPendingLoadItemResult;
+    private Bundle mPendingRootHints;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        sInstance = this;
+        sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
+        setSessionToken(sSession.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);
+        } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
+            Assert.assertNull(mPendingLoadChildrenResult);
+            mPendingLoadChildrenResult = result;
+            mPendingRootHints = getBrowserRootHints();
+            result.detach();
+        } else if (MEDIA_ID_INVALID.equals(parentMediaId)) {
+            result.sendResult(null);
+        }
+    }
+
+    @Override
+    public void onLoadItem(String itemId, Result<MediaItem> result) {
+        if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
+            mPendingLoadItemResult = result;
+            mPendingRootHints = getBrowserRootHints();
+            result.detach();
+            return;
+        }
+
+        if (MEDIA_ID_INVALID.equals(itemId)) {
+            result.sendResult(null);
+            return;
+        }
+
+        for (String id : MEDIA_ID_CHILDREN) {
+            if (id.equals(itemId)) {
+                result.sendResult(createMediaItem(id));
+                return;
+            }
+        }
+
+        // Test the case where onLoadItem is not implemented.
+        super.onLoadItem(itemId, result);
+    }
+
+    @Override
+    public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+        if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
+            result.sendResult(Collections.<MediaItem>emptyList());
+        } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
+            result.sendResult(null);
+        } else if (SEARCH_QUERY.equals(query)) {
+            List<MediaItem> items = new ArrayList<>();
+            for (String id : MEDIA_ID_CHILDREN) {
+                if (id.contains(query)) {
+                    items.add(createMediaItem(id));
+                }
+            }
+            result.sendResult(items);
+        }
+    }
+
+    public void sendDelayedNotifyChildrenChanged() {
+        if (mPendingLoadChildrenResult != null) {
+            mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
+            mPendingRootHints = null;
+            mPendingLoadChildrenResult = null;
+        }
+    }
+
+    public void sendDelayedItemLoaded() {
+        if (mPendingLoadItemResult != null) {
+            mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
+                    .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
+                    MediaItem.FLAG_BROWSABLE));
+            mPendingRootHints = null;
+            mPendingLoadItemResult = null;
+        }
+    }
+
+    private MediaItem createMediaItem(String id) {
+        return new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId(id).setExtras(getBrowserRootHints()).build(),
+                MediaItem.FLAG_BROWSABLE);
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
new file mode 100644
index 0000000..2598bef
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.media.session;
+
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.PollingCheck;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test {@link MediaControllerCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatTest {
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final String SESSION_TAG = "test-session";
+    private static final String EXTRAS_KEY = "test-key";
+    private static final String EXTRAS_VALUE = "test-val";
+    private static final float DELTA = 1e-4f;
+
+    private final Object mWaitLock = new Object();
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private MediaSessionCompat mSession;
+    private MediaSessionCallback mCallback = new MediaSessionCallback();
+    private MediaControllerCompat mController;
+
+    @Before
+    public void setUp() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession = new MediaSessionCompat(getContext(), SESSION_TAG);
+                mSession.setCallback(mCallback, mHandler);
+                mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+                mController = mSession.getController();
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.release();
+    }
+
+    @Test
+    @SmallTest
+    public void testGetPackageName() {
+        assertEquals(getContext().getPackageName(), mController.getPackageName());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetRatingType() {
+        assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+                RatingCompat.RATING_NONE, mController.getRatingType());
+
+        mSession.setRatingType(RatingCompat.RATING_5_STARS);
+        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
+            // Wait until the extra binder is ready.
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                protected boolean check() {
+                    return mController.getRatingType() != RatingCompat.RATING_NONE;
+                }
+            }.run();
+        }
+        assertEquals(RatingCompat.RATING_5_STARS, mController.getRatingType());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetSessionToken() throws Exception {
+        assertEquals(mSession.getSessionToken(), mController.getSessionToken());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCommand() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            final String command = "test-command";
+            final Bundle extras = new Bundle();
+            extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+            mController.sendCommand(command, extras, new ResultReceiver(null));
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCommandCalled);
+            assertNotNull(mCallback.mCommandCallback);
+            assertEquals(command, mCallback.mCommand);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testAddRemoveQueueItems() throws Exception {
+        final String mediaId = "media_id";
+        final String mediaTitle = "media_title";
+        MediaDescriptionCompat itemDescription = new MediaDescriptionCompat.Builder()
+                .setMediaId(mediaId).setTitle(mediaTitle).build();
+
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            mController.addQueueItem(itemDescription);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemCalled);
+            assertEquals(-1, mCallback.mQueueIndex);
+            assertEquals(mediaId, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            mController.addQueueItem(itemDescription, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemAtCalled);
+            assertEquals(0, mCallback.mQueueIndex);
+            assertEquals(mediaId, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            mController.removeQueueItemAt(0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRemoveQueueItemAtCalled);
+            assertEquals(0, mCallback.mQueueIndex);
+
+            mCallback.reset();
+            mController.removeQueueItem(itemDescription);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRemoveQueueItemCalled);
+            assertEquals(mediaId, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle, mCallback.mQueueDescription.getTitle());
+        }
+    }
+
+    // TODO: Uncomment after fixing this test. This test causes an Exception on System UI.
+    // @Test
+    // @SmallTest
+    public void testVolumeControl() throws Exception {
+        VolumeProviderCompat vp =
+                new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 11, 5) {
+            @Override
+            public void onSetVolumeTo(int volume) {
+                synchronized (mWaitLock) {
+                    setCurrentVolume(volume);
+                    mWaitLock.notify();
+                }
+            }
+
+            @Override
+            public void onAdjustVolume(int direction) {
+                synchronized (mWaitLock) {
+                    switch (direction) {
+                        case AudioManager.ADJUST_LOWER:
+                            setCurrentVolume(getCurrentVolume() - 1);
+                            break;
+                        case AudioManager.ADJUST_RAISE:
+                            setCurrentVolume(getCurrentVolume() + 1);
+                            break;
+                    }
+                    mWaitLock.notify();
+                }
+            }
+        };
+        mSession.setPlaybackToRemote(vp);
+
+        synchronized (mWaitLock) {
+            // test setVolumeTo
+            mController.setVolumeTo(7, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(7, vp.getCurrentVolume());
+
+            // test adjustVolume
+            mController.adjustVolume(AudioManager.ADJUST_LOWER, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(6, vp.getCurrentVolume());
+
+            mController.adjustVolume(AudioManager.ADJUST_RAISE, 0);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(7, vp.getCurrentVolume());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testTransportControlsAndMediaSessionCallback() throws Exception {
+        MediaControllerCompat.TransportControls controls = mController.getTransportControls();
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            controls.play();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayCalled);
+
+            mCallback.reset();
+            controls.pause();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPauseCalled);
+
+            mCallback.reset();
+            controls.stop();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnStopCalled);
+
+            mCallback.reset();
+            controls.fastForward();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnFastForwardCalled);
+
+            mCallback.reset();
+            controls.rewind();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRewindCalled);
+
+            mCallback.reset();
+            controls.skipToPrevious();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+            mCallback.reset();
+            controls.skipToNext();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToNextCalled);
+
+            mCallback.reset();
+            final long seekPosition = 1000;
+            controls.seekTo(seekPosition);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSeekToCalled);
+            assertEquals(seekPosition, mCallback.mSeekPosition);
+
+            mCallback.reset();
+            final RatingCompat rating =
+                    RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+            controls.setRating(rating);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRatingCalled);
+            assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+            assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+            mCallback.reset();
+            final String mediaId = "test-media-id";
+            final Bundle extras = new Bundle();
+            extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+            controls.playFromMediaId(mediaId, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final String query = "test-query";
+            controls.playFromSearch(query, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final Uri uri = Uri.parse("content://test/popcorn.mod");
+            controls.playFromUri(uri, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final String action = "test-action";
+            controls.sendCustomAction(action, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            mCallback.mOnCustomActionCalled = false;
+            final PlaybackStateCompat.CustomAction customAction =
+                    new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+                            .setExtras(extras)
+                            .build();
+            controls.sendCustomAction(customAction, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final long queueItemId = 1000;
+            controls.skipToQueueItem(queueItemId);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToQueueItemCalled);
+            assertEquals(queueItemId, mCallback.mQueueItemId);
+
+            mCallback.reset();
+            controls.prepare();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareCalled);
+
+            mCallback.reset();
+            controls.prepareFromMediaId(mediaId, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            controls.prepareFromSearch(query, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            controls.prepareFromUri(uri, extras);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+            mCallback.reset();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            controls.setRepeatMode(repeatMode);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRepeatModeCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+
+            mCallback.reset();
+            final boolean shuffleModeEnabled = true;
+            controls.setShuffleModeEnabled(shuffleModeEnabled);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetShuffleModeEnabledCalled);
+            assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testPlaybackInfo() {
+        final int playbackType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
+        final int volumeControl = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        final int maxVolume = 10;
+        final int currentVolume = 3;
+
+        int audioStream = 77;
+        MediaControllerCompat.PlaybackInfo info = new MediaControllerCompat.PlaybackInfo(
+                playbackType, audioStream, volumeControl, maxVolume, currentVolume);
+
+        assertEquals(playbackType, info.getPlaybackType());
+        assertEquals(audioStream, info.getAudioStream());
+        assertEquals(volumeControl, info.getVolumeControl());
+        assertEquals(maxVolume, info.getMaxVolume());
+        assertEquals(currentVolume, info.getCurrentVolume());
+    }
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private long mSeekPosition;
+        private long mQueueItemId;
+        private RatingCompat mRating;
+        private String mMediaId;
+        private String mQuery;
+        private Uri mUri;
+        private String mAction;
+        private String mCommand;
+        private Bundle mExtras;
+        private ResultReceiver mCommandCallback;
+        private int mRepeatMode;
+        private boolean mShuffleModeEnabled;
+        private int mQueueIndex;
+        private MediaDescriptionCompat mQueueDescription;
+
+        private boolean mOnPlayCalled;
+        private boolean mOnPauseCalled;
+        private boolean mOnStopCalled;
+        private boolean mOnFastForwardCalled;
+        private boolean mOnRewindCalled;
+        private boolean mOnSkipToPreviousCalled;
+        private boolean mOnSkipToNextCalled;
+        private boolean mOnSeekToCalled;
+        private boolean mOnSkipToQueueItemCalled;
+        private boolean mOnSetRatingCalled;
+        private boolean mOnPlayFromMediaIdCalled;
+        private boolean mOnPlayFromSearchCalled;
+        private boolean mOnPlayFromUriCalled;
+        private boolean mOnCustomActionCalled;
+        private boolean mOnCommandCalled;
+        private boolean mOnPrepareCalled;
+        private boolean mOnPrepareFromMediaIdCalled;
+        private boolean mOnPrepareFromSearchCalled;
+        private boolean mOnPrepareFromUriCalled;
+        private boolean mOnSetRepeatModeCalled;
+        private boolean mOnSetShuffleModeEnabledCalled;
+        private boolean mOnAddQueueItemCalled;
+        private boolean mOnAddQueueItemAtCalled;
+        private boolean mOnRemoveQueueItemCalled;
+        private boolean mOnRemoveQueueItemAtCalled;
+
+        public void reset() {
+            mSeekPosition = -1;
+            mQueueItemId = -1;
+            mRating = null;
+            mMediaId = null;
+            mQuery = null;
+            mUri = null;
+            mAction = null;
+            mExtras = null;
+            mCommand = null;
+            mCommandCallback = null;
+            mShuffleModeEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mQueueIndex = -1;
+            mQueueDescription = null;
+
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
+            mOnSkipToQueueItemCalled = false;
+            mOnSeekToCalled = false;
+            mOnSetRatingCalled = false;
+            mOnPlayFromMediaIdCalled = false;
+            mOnPlayFromSearchCalled = false;
+            mOnPlayFromUriCalled = false;
+            mOnCustomActionCalled = false;
+            mOnCommandCalled = false;
+            mOnPrepareCalled = false;
+            mOnPrepareFromMediaIdCalled = false;
+            mOnPrepareFromSearchCalled = false;
+            mOnPrepareFromUriCalled = false;
+            mOnSetRepeatModeCalled = false;
+            mOnSetShuffleModeEnabledCalled = false;
+            mOnAddQueueItemCalled = false;
+            mOnAddQueueItemAtCalled = false;
+            mOnRemoveQueueItemCalled = false;
+            mOnRemoveQueueItemAtCalled = false;
+        }
+
+        @Override
+        public void onPlay() {
+            synchronized (mWaitLock) {
+                mOnPlayCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPause() {
+            synchronized (mWaitLock) {
+                mOnPauseCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onStop() {
+            synchronized (mWaitLock) {
+                mOnStopCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onFastForward() {
+            synchronized (mWaitLock) {
+                mOnFastForwardCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRewind() {
+            synchronized (mWaitLock) {
+                mOnRewindCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            synchronized (mWaitLock) {
+                mOnSkipToPreviousCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToNext() {
+            synchronized (mWaitLock) {
+                mOnSkipToNextCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            synchronized (mWaitLock) {
+                mOnSeekToCalled = true;
+                mSeekPosition = pos;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRating(RatingCompat rating) {
+            synchronized (mWaitLock) {
+                mOnSetRatingCalled = true;
+                mRating = rating;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnCustomActionCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToQueueItem(long id) {
+            synchronized (mWaitLock) {
+                mOnSkipToQueueItemCalled = true;
+                mQueueItemId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+            synchronized (mWaitLock) {
+                mOnCommandCalled = true;
+                mCommand = command;
+                mExtras = extras;
+                mCommandCallback = cb;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepare() {
+            synchronized (mWaitLock) {
+                mOnPrepareCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRepeatMode(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnSetRepeatModeCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemCalled = true;
+                mQueueDescription = description;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemAtCalled = true;
+                mQueueIndex = index;
+                mQueueDescription = description;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRemoveQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnRemoveQueueItemCalled = true;
+                mQueueDescription = description;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetShuffleModeEnabled(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnSetShuffleModeEnabledCalled = true;
+                mShuffleModeEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRemoveQueueItemAt(int index) {
+            synchronized (mWaitLock) {
+                mOnRemoveQueueItemAtCalled = true;
+                mQueueIndex = index;
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
index 455a706..0e85c1c 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -16,84 +16,827 @@
 
 package android.support.v4.media.session;
 
-import android.app.Activity;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
 import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.PollingCheck;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.view.KeyEvent;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
-import static junit.framework.Assert.fail;
+import java.util.ArrayList;
+import java.util.List;
 
+/**
+ * Test {@link MediaSessionCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
 public class MediaSessionCompatTest {
-    @Rule
-    public ActivityTestRule<TestActivity> mActivityRule =
-            new ActivityTestRule<>(TestActivity.class);
-    Context mContext;
-    Map<String, LockedObject> results = new HashMap<>();
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+    private static final String TEST_SESSION_TAG = "test-session-tag";
+    private static final String TEST_KEY = "test-key";
+    private static final String TEST_VALUE = "test-val";
+    private static final Bundle TEST_BUNDLE = createTestBundle();
+    private static final String TEST_SESSION_EVENT = "test-session-event";
+    private static final int TEST_CURRENT_VOLUME = 10;
+    private static final int TEST_MAX_VOLUME = 11;
+    private static final long TEST_QUEUE_ID = 12L;
+    private static final long TEST_ACTION = 55L;
+    private static final int TEST_ERROR_CODE =
+            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED;
+    private static final String TEST_ERROR_MSG = "test-error-msg";
+
+    private static Bundle createTestBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putString(TEST_KEY, TEST_VALUE);
+        return bundle;
+    }
+
+    private AudioManager mAudioManager;
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private Object mWaitLock = new Object();
+    private MediaControllerCallback mCallback = new MediaControllerCallback();
+    private MediaSessionCompat mSession;
 
     @Before
-    public void setUp() {
-        mContext = InstrumentationRegistry.getContext();
+    public void setUp() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+                mSession = new MediaSessionCompat(getContext(), TEST_SESSION_TAG);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // It is OK to call release() twice.
+        mSession.release();
+        mSession = null;
+    }
+
+    /**
+     * Tests that a session can be created and that all the fields are
+     * initialized correctly.
+     */
+    @Test
+    @SmallTest
+    public void testCreateSession() throws Exception {
+        assertNotNull(mSession.getSessionToken());
+        assertFalse("New session should not be active", mSession.isActive());
+
+        // Verify by getting the controller and checking all its fields
+        MediaControllerCompat controller = mSession.getController();
+        assertNotNull(controller);
+        verifyNewSession(controller, TEST_SESSION_TAG);
+    }
+
+    /**
+     * Tests MediaSessionCompat.Token created in the constructor of MediaSessionCompat.
+     */
+    @Test
+    @SmallTest
+    public void testSessionToken() throws Exception {
+        MediaSessionCompat.Token sessionToken = mSession.getSessionToken();
+
+        assertNotNull(sessionToken);
+        assertEquals(0, sessionToken.describeContents());
+
+        // Test writeToParcel
+        Parcel p = Parcel.obtain();
+        sessionToken.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        MediaSessionCompat.Token token = MediaSessionCompat.Token.CREATOR.createFromParcel(p);
+        assertEquals(token, sessionToken);
+        p.recycle();
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setExtras}.
+     */
+    @Test
+    @SmallTest
+    public void testSetExtras() throws Exception {
+        final Bundle extras = new Bundle();
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            mSession.setExtras(TEST_BUNDLE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnExtraChangedCalled);
+
+            Bundle extrasOut = mCallback.mExtras;
+            assertNotNull(extrasOut);
+            assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+
+            extrasOut = controller.getExtras();
+            assertNotNull(extrasOut);
+            assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setFlags}.
+     */
+    @Test
+    @SmallTest
+    public void testSetFlags() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            mSession.setFlags(5);
+            assertEquals(5, controller.getFlags());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setMetadata}.
+     */
+    @Test
+    @SmallTest
+    public void testSetMetadata() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            MediaMetadataCompat metadata =
+                    new MediaMetadataCompat.Builder().putString(TEST_KEY, TEST_VALUE).build();
+            mSession.setMetadata(metadata);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            metadataOut = controller.getMetadata();
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackState}.
+     */
+    @Test
+    @SmallTest
+    public void testSetPlaybackState() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            waitUntilExtraBinderReady(controller);
+        }
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            PlaybackStateCompat state =
+                    new PlaybackStateCompat.Builder()
+                            .setActions(TEST_ACTION)
+                            .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+                            .build();
+            mSession.setPlaybackState(state);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlaybackStateChangedCalled);
+
+            PlaybackStateCompat stateOut = mCallback.mPlaybackState;
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+
+            stateOut = controller.getPlaybackState();
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setQueue} and {@link MediaSessionCompat#setQueueTitle}.
+     */
+    @Test
+    @SmallTest
+    public void testSetQueueAndSetQueueTitle() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
+            MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+                    new MediaDescriptionCompat.Builder()
+                            .setMediaId(TEST_VALUE)
+                            .setTitle("title")
+                            .build(),
+                    TEST_QUEUE_ID);
+            queue.add(item);
+            mSession.setQueue(queue);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueChangedCalled);
+
+            mSession.setQueueTitle(TEST_VALUE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+            assertEquals(TEST_VALUE, mCallback.mTitle);
+            assertEquals(queue.size(), mCallback.mQueue.size());
+            assertEquals(TEST_QUEUE_ID, mCallback.mQueue.get(0).getQueueId());
+            assertEquals(TEST_VALUE, mCallback.mQueue.get(0).getDescription().getMediaId());
+
+            assertEquals(TEST_VALUE, controller.getQueueTitle());
+            assertEquals(queue.size(), controller.getQueue().size());
+            assertEquals(TEST_QUEUE_ID, controller.getQueue().get(0).getQueueId());
+            assertEquals(TEST_VALUE, controller.getQueue().get(0).getDescription().getMediaId());
+
+            mCallback.resetLocked();
+            mSession.setQueue(null);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueChangedCalled);
+
+            mSession.setQueueTitle(null);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+            assertNull(mCallback.mTitle);
+            assertNull(mCallback.mQueue);
+            assertNull(controller.getQueueTitle());
+            assertNull(controller.getQueue());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setSessionActivity}.
+     */
+    @Test
+    @SmallTest
+    public void testSessionActivity() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        synchronized (mWaitLock) {
+            Intent intent = new Intent("cts.MEDIA_SESSION_ACTION");
+            PendingIntent pi = PendingIntent.getActivity(getContext(), 555, intent, 0);
+            mSession.setSessionActivity(pi);
+            assertEquals(pi, controller.getSessionActivity());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setRepeatMode}.
+     */
+    @Test
+    @SmallTest
+    public void testSetRepeatMode() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            waitUntilExtraBinderReady(controller);
+        }
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            mSession.setRepeatMode(repeatMode);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRepeatModeChangedCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+            assertEquals(repeatMode, controller.getRepeatMode());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setShuffleModeEnabled}.
+     */
+    @Test
+    @SmallTest
+    public void testSetShuffleModeEnabled() throws Exception {
+        final boolean shuffleModeEnabled = true;
+        MediaControllerCompat controller = mSession.getController();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            waitUntilExtraBinderReady(controller);
+        }
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            mSession.setShuffleModeEnabled(shuffleModeEnabled);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnShuffleModeChangedCalled);
+            assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+            assertEquals(shuffleModeEnabled, controller.isShuffleModeEnabled());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#sendSessionEvent}.
+     */
+    @Test
+    @SmallTest
+    public void testSendSessionEvent() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
+                Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+            waitUntilExtraBinderReady(controller);
+        }
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mCallback.resetLocked();
+            mSession.sendSessionEvent(TEST_SESSION_EVENT, TEST_BUNDLE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSessionEventCalled);
+            assertEquals(TEST_SESSION_EVENT, mCallback.mEvent);
+            assertEquals(TEST_VALUE, mCallback.mExtras.getString(TEST_KEY));
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setActive} and {@link MediaSessionCompat#release}.
+     */
+    @Test
+    @SmallTest
+    public void testSetActiveAndRelease() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            mSession.setActive(true);
+            assertTrue(mSession.isActive());
+
+            mCallback.resetLocked();
+            mSession.release();
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSessionDestroyedCalled);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
+     * {@link MediaSessionCompat#setPlaybackToRemote}.
+     */
+    @Test
+    @SmallTest
+    public void testPlaybackToLocalAndRemote() throws Exception {
+        MediaControllerCompat controller = mSession.getController();
+        controller.registerCallback(mCallback, mHandler);
+        synchronized (mWaitLock) {
+            // test setPlaybackToRemote, do this before testing setPlaybackToLocal
+            // to ensure it switches correctly.
+            mCallback.resetLocked();
+            try {
+                mSession.setPlaybackToRemote(null);
+                fail("Expected IAE for setPlaybackToRemote(null)");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            VolumeProviderCompat vp = new VolumeProviderCompat(
+                    VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    TEST_MAX_VOLUME,
+                    TEST_CURRENT_VOLUME) {};
+            mSession.setPlaybackToRemote(vp);
+
+            MediaControllerCompat.PlaybackInfo info = null;
+            for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+                mCallback.mOnAudioInfoChangedCalled = false;
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mCallback.mOnAudioInfoChangedCalled);
+                info = mCallback.mPlaybackInfo;
+                if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+                        && info.getMaxVolume() == TEST_MAX_VOLUME
+                        && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+                        && info.getPlaybackType()
+                        == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                    break;
+                }
+            }
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    info.getVolumeControl());
+
+            info = controller.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+            // test setPlaybackToLocal
+            mSession.setPlaybackToLocal(AudioManager.STREAM_RING);
+            info = controller.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                    info.getPlaybackType());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat.Callback#onMediaButtonEvent}.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackOnMediaButtonEvent() throws Exception {
+        MediaSessionCallback sessionCallback = new MediaSessionCallback();
+        mSession.setCallback(sessionCallback, new Handler(Looper.getMainLooper()));
+        mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
+        mSession.setActive(true);
+
+        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(
+                new ComponentName(getContext(), getContext().getClass()));
+        PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, mediaButtonIntent, 0);
+        mSession.setMediaButtonReceiver(pi);
+
+        long supportedActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
+                | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP
+                | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+                | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+
+        // Set state to STATE_PLAYING to get higher priority.
+        PlaybackStateCompat defaultState = new PlaybackStateCompat.Builder()
+                .setActions(supportedActions)
+                .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f)
+                .build();
+        mSession.setPlaybackState(defaultState);
+
+        synchronized (mWaitLock) {
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPlayCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPauseCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_NEXT);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnSkipToNextCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnSkipToPreviousCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_STOP);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnStopCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnFastForwardCalled);
+
+            sessionCallback.reset();
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_REWIND);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnRewindCalled);
+
+            // Test PLAY_PAUSE button twice.
+            // First, send PLAY_PAUSE button event while in STATE_PAUSED.
+            sessionCallback.reset();
+            mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+                    .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPlayCalled);
+
+            // Next, send PLAY_PAUSE button event while in STATE_PLAYING.
+            sessionCallback.reset();
+            mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+                    .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f).build());
+            sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(sessionCallback.mOnPauseCalled);
+        }
     }
 
     @Test
+    @SmallTest
     public void testSetNullCallback() throws Throwable {
-        initWait("testSetNullCallback");
-        mActivityRule.runOnUiThread(new Runnable() {
+        getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
                 try {
-                    MediaSessionCompat session = new MediaSessionCompat(mContext, "TEST");
+                    MediaSessionCompat session = new MediaSessionCompat(getContext(), "TEST");
                     session.setCallback(null);
                 } catch (Exception e) {
                     fail("Fail with an exception: " + e);
-                } finally {
-                    setResultData("testSetNullCallback", true);
                 }
             }
         });
-        waitFor("testSetNullCallback");
     }
 
-    private void initWait(String key) throws InterruptedException {
-        results.put(key, new LockedObject());
+    /**
+     * Tests {@link MediaSessionCompat.QueueItem}.
+     */
+    @Test
+    @SmallTest
+    public void testQueueItem() {
+        MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+                new MediaDescriptionCompat.Builder()
+                        .setMediaId("media-id")
+                        .setTitle("title")
+                        .build(),
+                TEST_QUEUE_ID);
+        assertEquals(TEST_QUEUE_ID, item.getQueueId());
+        assertEquals("media-id", item.getDescription().getMediaId());
+        assertEquals("title", item.getDescription().getTitle());
+        assertEquals(0, item.describeContents());
+
+        Parcel p = Parcel.obtain();
+        item.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        MediaSessionCompat.QueueItem other =
+                MediaSessionCompat.QueueItem.CREATOR.createFromParcel(p);
+        assertEquals(item.toString(), other.toString());
+        p.recycle();
     }
 
-    private Object[] waitFor(String key) throws InterruptedException {
-        return results.get(key).waitFor();
+    /**
+     * Verifies that a new session hasn't had any configuration bits set yet.
+     *
+     * @param controller The controller for the session
+     */
+    private void verifyNewSession(MediaControllerCompat controller, String tag) {
+        assertEquals("New session has unexpected configuration", 0L, controller.getFlags());
+        assertNull("New session has unexpected configuration", controller.getExtras());
+        assertNull("New session has unexpected configuration", controller.getMetadata());
+        assertEquals("New session has unexpected configuration",
+                getContext().getPackageName(), controller.getPackageName());
+        assertNull("New session has unexpected configuration", controller.getPlaybackState());
+        assertNull("New session has unexpected configuration", controller.getQueue());
+        assertNull("New session has unexpected configuration", controller.getQueueTitle());
+        assertEquals("New session has unexpected configuration", RatingCompat.RATING_NONE,
+                controller.getRatingType());
+        assertNull("New session has unexpected configuration", controller.getSessionActivity());
+
+        assertNotNull(controller.getSessionToken());
+        assertNotNull(controller.getTransportControls());
+
+        MediaControllerCompat.PlaybackInfo info = controller.getPlaybackInfo();
+        assertNotNull(info);
+        assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                info.getPlaybackType());
+        assertEquals(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
+                info.getCurrentVolume());
     }
 
-    private void setResultData(String key, Object... args) {
-        if (results.containsKey(key)) {
-            results.get(key).set(args);
+    private void sendMediaKeyInputToController(int keyCode) {
+        MediaControllerCompat controller = mSession.getController();
+        controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+        controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+    }
+
+    private void waitUntilExtraBinderReady(final MediaControllerCompat controller) {
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return controller.isExtraBinderReady();
+            }
+        }.run();
+    }
+
+    private class MediaControllerCallback extends MediaControllerCompat.Callback {
+        private volatile boolean mOnPlaybackStateChangedCalled;
+        private volatile boolean mOnMetadataChangedCalled;
+        private volatile boolean mOnQueueChangedCalled;
+        private volatile boolean mOnQueueTitleChangedCalled;
+        private volatile boolean mOnExtraChangedCalled;
+        private volatile boolean mOnAudioInfoChangedCalled;
+        private volatile boolean mOnSessionDestroyedCalled;
+        private volatile boolean mOnSessionEventCalled;
+        private volatile boolean mOnRepeatModeChangedCalled;
+        private volatile boolean mOnShuffleModeChangedCalled;
+
+        private volatile PlaybackStateCompat mPlaybackState;
+        private volatile MediaMetadataCompat mMediaMetadata;
+        private volatile List<MediaSessionCompat.QueueItem> mQueue;
+        private volatile CharSequence mTitle;
+        private volatile String mEvent;
+        private volatile Bundle mExtras;
+        private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+        private volatile int mRepeatMode;
+        private volatile boolean mShuffleModeEnabled;
+
+        public void resetLocked() {
+            mOnPlaybackStateChangedCalled = false;
+            mOnMetadataChangedCalled = false;
+            mOnQueueChangedCalled = false;
+            mOnQueueTitleChangedCalled = false;
+            mOnExtraChangedCalled = false;
+            mOnAudioInfoChangedCalled = false;
+            mOnSessionDestroyedCalled = false;
+            mOnSessionEventCalled = false;
+            mOnRepeatModeChangedCalled = false;
+            mOnShuffleModeChangedCalled = false;
+
+            mPlaybackState = null;
+            mMediaMetadata = null;
+            mQueue = null;
+            mTitle = null;
+            mExtras = null;
+            mPlaybackInfo = null;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleModeEnabled = false;
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mWaitLock) {
+                mOnPlaybackStateChangedCalled = true;
+                mPlaybackState = state;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mWaitLock) {
+                mOnMetadataChangedCalled = true;
+                mMediaMetadata = metadata;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueChanged(List<MediaSessionCompat.QueueItem> queue) {
+            synchronized (mWaitLock) {
+                mOnQueueChangedCalled = true;
+                mQueue = queue;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueTitleChanged(CharSequence title) {
+            synchronized (mWaitLock) {
+                mOnQueueTitleChangedCalled = true;
+                mTitle = title;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnExtraChangedCalled = true;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+            synchronized (mWaitLock) {
+                mOnAudioInfoChangedCalled = true;
+                mPlaybackInfo = info;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            synchronized (mWaitLock) {
+                mOnSessionDestroyedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSessionEventCalled = true;
+                mEvent = event;
+                mExtras = (Bundle) extras.clone();
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRepeatModeChanged(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnRepeatModeChangedCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onShuffleModeChanged(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnShuffleModeChangedCalled = true;
+                mShuffleModeEnabled = enabled;
+                mWaitLock.notify();
+            }
         }
     }
 
-    private class LockedObject {
-        private Semaphore mLock = new Semaphore(1);
-        private volatile Object[] mArgs;
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private boolean mOnPlayCalled;
+        private boolean mOnPauseCalled;
+        private boolean mOnStopCalled;
+        private boolean mOnFastForwardCalled;
+        private boolean mOnRewindCalled;
+        private boolean mOnSkipToPreviousCalled;
+        private boolean mOnSkipToNextCalled;
 
-        public LockedObject() {
-            mLock.drainPermits();
+        public void reset() {
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
         }
 
-        public void set(Object... args) {
-            mArgs = args;
-            mLock.release(1);
+        @Override
+        public void onPlay() {
+            synchronized (mWaitLock) {
+                mOnPlayCalled = true;
+                mWaitLock.notify();
+            }
         }
 
-        public Object[] waitFor() throws InterruptedException {
-            mLock.tryAcquire(1, 2, TimeUnit.SECONDS);
-            return mArgs;
+        @Override
+        public void onPause() {
+            synchronized (mWaitLock) {
+                mOnPauseCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onStop() {
+            synchronized (mWaitLock) {
+                mOnStopCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onFastForward() {
+            synchronized (mWaitLock) {
+                mOnFastForwardCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRewind() {
+            synchronized (mWaitLock) {
+                mOnRewindCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            synchronized (mWaitLock) {
+                mOnSkipToPreviousCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToNext() {
+            synchronized (mWaitLock) {
+                mOnSkipToNextCalled = true;
+                mWaitLock.notify();
+            }
         }
     }
 }
diff --git a/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
new file mode 100644
index 0000000..9e320cd
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * Test {@link PlaybackStateCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class PlaybackStateCompatTest {
+
+    private static final long TEST_POSITION = 20000L;
+    private static final long TEST_BUFFERED_POSITION = 15000L;
+    private static final long TEST_UPDATE_TIME = 100000L;
+    private static final long TEST_ACTIONS = PlaybackStateCompat.ACTION_PLAY
+            | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SEEK_TO;
+    private static final long TEST_QUEUE_ITEM_ID = 23L;
+    private static final float TEST_PLAYBACK_SPEED = 3.0f;
+    private static final float TEST_PLAYBACK_SPEED_ON_REWIND = -2.0f;
+    private static final float DELTA = 1e-7f;
+
+    private static final int TEST_ERROR_CODE =
+            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED;
+    private static final String TEST_ERROR_MSG = "test-error-msg";
+    private static final String TEST_CUSTOM_ACTION = "test-custom-action";
+    private static final String TEST_CUSTOM_ACTION_NAME = "test-custom-action-name";
+    private static final int TEST_ICON_RESOURCE_ID = android.R.drawable.ic_media_next;
+
+    private static final String EXTRAS_KEY = "test-key";
+    private static final String EXTRAS_VALUE = "test-value";
+
+    /**
+     * Test default values of {@link PlaybackStateCompat}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder() {
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder().build();
+
+        assertEquals(new ArrayList<PlaybackStateCompat.CustomAction>(), state.getCustomActions());
+        assertEquals(0, state.getState());
+        assertEquals(0L, state.getPosition());
+        assertEquals(0L, state.getBufferedPosition());
+        assertEquals(0.0f, state.getPlaybackSpeed(), DELTA);
+        assertEquals(0L, state.getActions());
+        assertEquals(0, state.getErrorCode());
+        assertNull(state.getErrorMessage());
+        assertEquals(0L, state.getLastPositionUpdateTime());
+        assertEquals(MediaSessionCompat.QueueItem.UNKNOWN_ID, state.getActiveQueueItemId());
+        assertNull(state.getExtras());
+    }
+
+    /**
+     * Test following setter methods of {@link PlaybackStateCompat.Builder}:
+     * {@link PlaybackStateCompat.Builder#setState(int, long, float)}
+     * {@link PlaybackStateCompat.Builder#setActions(long)}
+     * {@link PlaybackStateCompat.Builder#setActiveQueueItemId(long)}
+     * {@link PlaybackStateCompat.Builder#setBufferedPosition(long)}
+     * {@link PlaybackStateCompat.Builder#setErrorMessage(CharSequence)}
+     * {@link PlaybackStateCompat.Builder#setExtras(Bundle)}
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_setterMethods() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+                .setState(PlaybackStateCompat.STATE_PLAYING, TEST_POSITION, TEST_PLAYBACK_SPEED)
+                .setActions(TEST_ACTIONS)
+                .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+                .setBufferedPosition(TEST_BUFFERED_POSITION)
+                .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+                .setExtras(extras)
+                .build();
+        assertEquals(PlaybackStateCompat.STATE_PLAYING, state.getState());
+        assertEquals(TEST_POSITION, state.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED, state.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_ACTIONS, state.getActions());
+        assertEquals(TEST_QUEUE_ITEM_ID, state.getActiveQueueItemId());
+        assertEquals(TEST_BUFFERED_POSITION, state.getBufferedPosition());
+        assertEquals(TEST_ERROR_CODE, state.getErrorCode());
+        assertEquals(TEST_ERROR_MSG, state.getErrorMessage().toString());
+        assertNotNull(state.getExtras());
+        assertEquals(EXTRAS_VALUE, state.getExtras().get(EXTRAS_KEY));
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#setState(int, long, float, long)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_setStateWithUpdateTime() {
+        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+                .setState(
+                        PlaybackStateCompat.STATE_REWINDING,
+                        TEST_POSITION,
+                        TEST_PLAYBACK_SPEED_ON_REWIND,
+                        TEST_UPDATE_TIME)
+                .build();
+        assertEquals(PlaybackStateCompat.STATE_REWINDING, state.getState());
+        assertEquals(TEST_POSITION, state.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED_ON_REWIND, state.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_UPDATE_TIME, state.getLastPositionUpdateTime());
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#addCustomAction(String, String, int)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_addCustomAction() {
+        ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+        for (int i = 0; i < 5; i++) {
+            actions.add(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .build());
+            builder.addCustomAction(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i);
+        }
+
+        PlaybackStateCompat state = builder.build();
+        assertEquals(actions.size(), state.getCustomActions().size());
+        for (int i = 0; i < actions.size(); i++) {
+            assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+        }
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}.
+     */
+    @Test
+    @SmallTest
+    public void testBuilder_addCustomActionWithCustomActionObject() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+        for (int i = 0; i < 5; i++) {
+            actions.add(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .setExtras(extras)
+                    .build());
+            builder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
+                    TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+                    .setExtras(extras)
+                    .build());
+        }
+
+        PlaybackStateCompat state = builder.build();
+        assertEquals(actions.size(), state.getCustomActions().size());
+        for (int i = 0; i < actions.size(); i++) {
+            assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+        }
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat#writeToParcel(Parcel, int)}.
+     */
+    @Test
+    @SmallTest
+    public void testWriteToParcel() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        PlaybackStateCompat.Builder builder =
+                new PlaybackStateCompat.Builder()
+                        .setState(PlaybackStateCompat.STATE_CONNECTING, TEST_POSITION,
+                                TEST_PLAYBACK_SPEED, TEST_UPDATE_TIME)
+                        .setActions(TEST_ACTIONS)
+                        .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+                        .setBufferedPosition(TEST_BUFFERED_POSITION)
+                        .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+                        .setExtras(extras);
+
+        for (int i = 0; i < 5; i++) {
+            builder.addCustomAction(
+                    new PlaybackStateCompat.CustomAction.Builder(
+                            TEST_CUSTOM_ACTION + i,
+                            TEST_CUSTOM_ACTION_NAME + i,
+                            TEST_ICON_RESOURCE_ID + i)
+                            .setExtras(extras)
+                            .build());
+        }
+        PlaybackStateCompat state = builder.build();
+
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        PlaybackStateCompat stateOut = PlaybackStateCompat.CREATOR.createFromParcel(parcel);
+        assertEquals(PlaybackStateCompat.STATE_CONNECTING, stateOut.getState());
+        assertEquals(TEST_POSITION, stateOut.getPosition());
+        assertEquals(TEST_PLAYBACK_SPEED, stateOut.getPlaybackSpeed(), DELTA);
+        assertEquals(TEST_UPDATE_TIME, stateOut.getLastPositionUpdateTime());
+        assertEquals(TEST_BUFFERED_POSITION, stateOut.getBufferedPosition());
+        assertEquals(TEST_ACTIONS, stateOut.getActions());
+        assertEquals(TEST_QUEUE_ITEM_ID, stateOut.getActiveQueueItemId());
+        assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+        assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage());
+        assertNotNull(stateOut.getExtras());
+        assertEquals(EXTRAS_VALUE, stateOut.getExtras().get(EXTRAS_KEY));
+
+        assertEquals(state.getCustomActions().size(), stateOut.getCustomActions().size());
+        for (int i = 0; i < state.getCustomActions().size(); i++) {
+            assertCustomActionEquals(
+                    state.getCustomActions().get(i), stateOut.getCustomActions().get(i));
+        }
+        parcel.recycle();
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat#describeContents()}.
+     */
+    @Test
+    @SmallTest
+    public void testDescribeContents() {
+        assertEquals(0, new PlaybackStateCompat.Builder().build().describeContents());
+    }
+
+    /**
+     * Test {@link PlaybackStateCompat.CustomAction}.
+     */
+    @Test
+    @SmallTest
+    public void testCustomAction() {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+        // Test Builder/Getters
+        PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction
+                .Builder(TEST_CUSTOM_ACTION, TEST_CUSTOM_ACTION_NAME, TEST_ICON_RESOURCE_ID)
+                .setExtras(extras)
+                .build();
+        assertEquals(TEST_CUSTOM_ACTION, customAction.getAction());
+        assertEquals(TEST_CUSTOM_ACTION_NAME, customAction.getName().toString());
+        assertEquals(TEST_ICON_RESOURCE_ID, customAction.getIcon());
+        assertEquals(EXTRAS_VALUE, customAction.getExtras().get(EXTRAS_KEY));
+
+        // Test describeContents
+        assertEquals(0, customAction.describeContents());
+
+        // Test writeToParcel
+        Parcel parcel = Parcel.obtain();
+        customAction.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        assertCustomActionEquals(
+                customAction, PlaybackStateCompat.CustomAction.CREATOR.createFromParcel(parcel));
+        parcel.recycle();
+    }
+
+    private void assertCustomActionEquals(PlaybackStateCompat.CustomAction action1,
+            PlaybackStateCompat.CustomAction action2) {
+        assertEquals(action1.getAction(), action2.getAction());
+        assertEquals(action1.getName(), action2.getName());
+        assertEquals(action1.getIcon(), action2.getIcon());
+
+        // To be the same, two extras should be both null or both not null.
+        assertEquals(action1.getExtras() != null, action2.getExtras() != null);
+        if (action1.getExtras() != null) {
+            assertEquals(action1.getExtras().get(EXTRAS_KEY), action2.getExtras().get(EXTRAS_KEY));
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
deleted file mode 100644
index dd56467..0000000
--- a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media.session;
-
-import android.app.Activity;
-
-public class TestActivity extends Activity {
-}
diff --git a/percent/Android.mk b/percent/Android.mk
index b569224..aaeb65ad 100644
--- a/percent/Android.mk
+++ b/percent/Android.mk
@@ -27,6 +27,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := android-support-v4
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
diff --git a/percent/AndroidManifest-make.xml b/percent/AndroidManifest-make.xml
new file mode 100644
index 0000000..e979013
--- /dev/null
+++ b/percent/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.percent">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/percent/AndroidManifest.xml b/percent/AndroidManifest.xml
index e979013..0d55165 100644
--- a/percent/AndroidManifest.xml
+++ b/percent/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.percent">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/percent/build.gradle b/percent/build.gradle
index c3f386e..b120075 100644
--- a/percent/build.gradle
+++ b/percent/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'percent'
 
 dependencies {
@@ -53,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
index 81ff253..bbfc461 100644
--- a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
+++ b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
@@ -19,8 +19,6 @@
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
-import static org.junit.Assume.assumeTrue;
-
 import android.os.Build;
 import android.support.percent.test.R;
 import android.support.test.filters.SmallTest;
@@ -108,8 +106,6 @@
 
     @Before
     public void setUp() throws Exception {
-        assumeTrue(Build.VERSION.SDK_INT != 17);
-
         final TestRelativeRtlActivity activity = mActivityTestRule.getActivity();
         mPercentRelativeLayout = (PercentRelativeLayout) activity.findViewById(R.id.container);
         mContainerWidth = mPercentRelativeLayout.getWidth();
@@ -132,6 +128,9 @@
 
     @Test
     public void testTopChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_top);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -161,6 +160,9 @@
 
     @Test
     public void testStartChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_start);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -191,6 +193,9 @@
 
     @Test
     public void testBottomChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -222,6 +227,9 @@
 
     @Test
     public void testEndChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_end);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -252,6 +260,9 @@
 
     @Test
     public void testCenterChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
 
         boolean supportsRtl = Build.VERSION.SDK_INT >= 17;
diff --git a/recommendation/Android.mk b/recommendation/Android.mk
index 0e0a9d7..67721bb 100644
--- a/recommendation/Android.mk
+++ b/recommendation/Android.mk
@@ -27,6 +27,7 @@
 LOCAL_SDK_VERSION := 21
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v4 \
     android-support-annotations
diff --git a/recommendation/AndroidManifest-make.xml b/recommendation/AndroidManifest-make.xml
new file mode 100644
index 0000000..ef1223e
--- /dev/null
+++ b/recommendation/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.recommendation">
+    <uses-sdk android:minSdkVersion="21"/>
+    <application />
+</manifest>
diff --git a/recommendation/AndroidManifest.xml b/recommendation/AndroidManifest.xml
index ef1223e..e36c822 100644
--- a/recommendation/AndroidManifest.xml
+++ b/recommendation/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.recommendation">
     <uses-sdk android:minSdkVersion="21"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/recommendation/build.gradle b/recommendation/build.gradle
index 75d68d1..dadad58 100644
--- a/recommendation/build.gradle
+++ b/recommendation/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'recommendation'
 
 dependencies {
@@ -41,27 +40,10 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
index 9fac4ab..d23ce05 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
@@ -38,7 +38,9 @@
 import com.example.android.supportv4.R;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A Fragment that lists all the various browsable queues available
@@ -66,6 +68,7 @@
     private final List<MediaBrowserCompat.MediaItem> mMediaItems = new ArrayList<>();
 
     private boolean mCanLoadNewPage;
+    private final Set<Integer> mSubscribedPages = new HashSet<Integer>();
     private MediaBrowserCompat mMediaBrowser;
     private BrowseAdapter mBrowserAdapter;
 
@@ -234,9 +237,15 @@
     public void onStop() {
         super.onStop();
         mMediaBrowser.disconnect();
+        mSubscribedPages.clear();
     }
 
     private void loadPage(int page) {
+        Integer pageInteger = Integer.valueOf(page);
+        if (mSubscribedPages.contains(pageInteger)) {
+            return;
+        }
+        mSubscribedPages.add(pageInteger);
         Bundle options = new Bundle();
         options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
         options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, PAGE_SIZE);
diff --git a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
index fa9b0b6..e19609a 100644
--- a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
+++ b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
@@ -52,7 +52,7 @@
         buttonAdd.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                if (bottom.getMenu().size() < 5) {
+                if (bottom.getMenu().size() < bottom.getMaxItemCount()) {
                     MenuItem item = bottom.getMenu().add("Bananas");
                     item.setIcon(android.R.drawable.ic_lock_power_off);
                 }
@@ -62,7 +62,9 @@
         buttonRemove.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                bottom.getMenu().removeItem(0);
+                if (bottom.getMenu().size() > 0) {
+                    bottom.getMenu().removeItem(bottom.getMenu().getItem(0).getItemId());
+                }
             }
         });
         Button buttonTint = (Button) findViewById(R.id.button_tint);
@@ -81,13 +83,18 @@
             @Override
             public void onClick(View view) {
                 final int menuSize = bottom.getMenu().size();
+                if (menuSize < 1) {
+                    return;
+                }
                 int currentlySelected = 0;
                 for (int i = 0; i < menuSize; i++) {
                     if (bottom.getMenu().getItem(i).isChecked()) {
                         currentlySelected = i;
+                        break;
                     }
                 }
-                bottom.getMenu().getItem((currentlySelected + 1) % menuSize).setChecked(true);
+                int next = (currentlySelected + 1) % menuSize;
+                bottom.setSelectedItemId(bottom.getMenu().getItem(next).getItemId());
             }
         });
         final TextView selectedItem = (TextView) findViewById(R.id.selected_item);
@@ -111,5 +118,12 @@
                         return true;
                     }
                 });
+        bottom.setOnNavigationItemReselectedListener(
+                new BottomNavigationView.OnNavigationItemReselectedListener() {
+                    @Override
+                    public void onNavigationItemReselected(@NonNull MenuItem item) {
+                        selectedItem.setText("Reselected " + item.getTitle());
+                    }
+                });
     }
 }
diff --git a/samples/SupportLeanbackDemos/res/raw/browse.mp4 b/samples/SupportLeanbackDemos/res/raw/browse.mp4
index b841a48..3f709fb 100644
--- a/samples/SupportLeanbackDemos/res/raw/browse.mp4
+++ b/samples/SupportLeanbackDemos/res/raw/browse.mp4
Binary files differ
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
index 395d557..9b48371 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
@@ -18,8 +18,7 @@
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.Handler;
-import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
-import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
+import android.support.v17.leanback.app.DetailsFragmentBackgroundController;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -72,17 +71,15 @@
     private Action mActionBuy;
 
     private FullWidthDetailsOverviewSharedElementHelper mHelper;
-    private DetailsBackgroundParallaxHelper mParallaxHelper;
-    private DetailsFragmentVideoHelper mVideoHelper;
-    private BackgroundHelper mBackgroundHelper;
-    private int mBitmapMinVerticalOffset = -100;
-    private MediaPlayerGlue mMediaPlayerGlue;
+    private BackgroundHelper mBackgroundHelper; // used to download bitmap async.
+    private final DetailsFragmentBackgroundController mDetailsBackground =
+            new DetailsFragmentBackgroundController(this);
 
     private void initializeTest() {
         TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
                 .getSharedElementEnterTransition();
         TEST_OVERVIEW_ROW_ON_SECOND = !TEST_SHARED_ELEMENT_TRANSITION;
-        TEST_ENTRANCE_TRANSITION = true;
+        TEST_ENTRANCE_TRANSITION = false;
     }
 
     @Override
@@ -92,21 +89,15 @@
         initializeTest();
 
         mBackgroundHelper = new BackgroundHelper(getActivity());
-        mParallaxHelper = new DetailsBackgroundParallaxHelper.ParallaxBuilder(
-                getActivity(), getParallaxManager())
-                .setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
-                .build();
+        mDetailsBackground.enableParallax();
         if (TEST_BACKGROUND_PLAYER) {
-            mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
-            mMediaPlayerGlue.setHost(createPlaybackGlueHost());
-            mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
-            mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+            MediaPlayerGlue mediaPlayerGlue = new MediaPlayerGlue(getActivity());
+            mDetailsBackground.setupVideoPlayback(mediaPlayerGlue);
 
-            mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
-            mMediaPlayerGlue.setArtist("A Googleer");
-            mMediaPlayerGlue.setTitle("Diving with Sharks");
-            mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
-
+            mediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mediaPlayerGlue.setArtist("A Googleer");
+            mediaPlayerGlue.setTitle("Diving with Sharks");
+            mediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
         }
 
         final Context context = getActivity();
@@ -218,7 +209,6 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View view = super.onCreateView(inflater, container, savedInstanceState);
-        mBackgroundHelper.attachToView(getBackgroundView());
         return view;
     }
 
@@ -291,21 +281,18 @@
         super.onStart();
 
         // Restore background drawable in onStart():
-        mBackgroundHelper.setDrawable(mParallaxHelper.getDrawable());
         mBackgroundHelper.loadBitmap(R.drawable.spiderman,
                 new BackgroundHelper.BitmapLoadCallback() {
                 @Override
                 public void onBitmapLoaded(Bitmap bitmap) {
-                    mParallaxHelper.setCoverImageBitmap(bitmap);
+                    mDetailsBackground.setCoverBitmap(bitmap);
                 }
             });
     }
 
     @Override
     public void onStop() {
+        mDetailsBackground.setCoverBitmap(null);
         super.onStop();
-        if (TEST_BACKGROUND_PLAYER) {
-            mMediaPlayerGlue.pause();
-        }
     }
 }
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
index ba482b1..ef41be0 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
@@ -21,8 +21,7 @@
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.Handler;
-import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
-import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
+import android.support.v17.leanback.app.DetailsSupportFragmentBackgroundController;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -75,17 +74,15 @@
     private Action mActionBuy;
 
     private FullWidthDetailsOverviewSharedElementHelper mHelper;
-    private DetailsBackgroundParallaxHelper mParallaxHelper;
-    private DetailsFragmentVideoHelper mVideoHelper;
-    private BackgroundHelper mBackgroundHelper;
-    private int mBitmapMinVerticalOffset = -100;
-    private MediaPlayerGlue mMediaPlayerGlue;
+    private BackgroundHelper mBackgroundHelper; // used to download bitmap async.
+    private final DetailsSupportFragmentBackgroundController mDetailsBackground =
+            new DetailsSupportFragmentBackgroundController(this);
 
     private void initializeTest() {
         TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
                 .getSharedElementEnterTransition();
         TEST_OVERVIEW_ROW_ON_SECOND = !TEST_SHARED_ELEMENT_TRANSITION;
-        TEST_ENTRANCE_TRANSITION = true;
+        TEST_ENTRANCE_TRANSITION = false;
     }
 
     @Override
@@ -95,21 +92,15 @@
         initializeTest();
 
         mBackgroundHelper = new BackgroundHelper(getActivity());
-        mParallaxHelper = new DetailsBackgroundParallaxHelper.ParallaxBuilder(
-                getActivity(), getParallaxManager())
-                .setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
-                .build();
+        mDetailsBackground.enableParallax();
         if (TEST_BACKGROUND_PLAYER) {
-            mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
-            mMediaPlayerGlue.setHost(createPlaybackGlueHost());
-            mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
-            mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+            MediaPlayerGlue mediaPlayerGlue = new MediaPlayerGlue(getActivity());
+            mDetailsBackground.setupVideoPlayback(mediaPlayerGlue);
 
-            mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
-            mMediaPlayerGlue.setArtist("A Googleer");
-            mMediaPlayerGlue.setTitle("Diving with Sharks");
-            mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
-
+            mediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mediaPlayerGlue.setArtist("A Googleer");
+            mediaPlayerGlue.setTitle("Diving with Sharks");
+            mediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
         }
 
         final Context context = getActivity();
@@ -221,7 +212,6 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View view = super.onCreateView(inflater, container, savedInstanceState);
-        mBackgroundHelper.attachToView(getBackgroundView());
         return view;
     }
 
@@ -294,21 +284,18 @@
         super.onStart();
 
         // Restore background drawable in onStart():
-        mBackgroundHelper.setDrawable(mParallaxHelper.getDrawable());
         mBackgroundHelper.loadBitmap(R.drawable.spiderman,
                 new BackgroundHelper.BitmapLoadCallback() {
                 @Override
                 public void onBitmapLoaded(Bitmap bitmap) {
-                    mParallaxHelper.setCoverImageBitmap(bitmap);
+                    mDetailsBackground.setCoverBitmap(bitmap);
                 }
             });
     }
 
     @Override
     public void onStop() {
+        mDetailsBackground.setCoverBitmap(null);
         super.onStop();
-        if (TEST_BACKGROUND_PLAYER) {
-            mMediaPlayerGlue.pause();
-        }
     }
 }
diff --git a/transition/Android.mk b/transition/Android.mk
index c468ef1..aefedd7 100644
--- a/transition/Android.mk
+++ b/transition/Android.mk
@@ -34,6 +34,7 @@
     $(call all-java-files-under,api23) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations \
     android-support-v4
diff --git a/transition/AndroidManifest-make.xml b/transition/AndroidManifest-make.xml
new file mode 100644
index 0000000..672e1b1
--- /dev/null
+++ b/transition/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.transition">
+    <uses-sdk android:minSdkVersion="14"/>
+    <application />
+</manifest>
diff --git a/transition/AndroidManifest.xml b/transition/AndroidManifest.xml
index 672e1b1..1059f63 100644
--- a/transition/AndroidManifest.xml
+++ b/transition/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.transition">
     <uses-sdk android:minSdkVersion="14"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/transition/build.gradle b/transition/build.gradle
index 05675f9..2f47f83 100644
--- a/transition/build.gradle
+++ b/transition/build.gradle
@@ -61,28 +61,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v13/Android.mk b/v13/Android.mk
index 1b30d99..9411930 100644
--- a/v13/Android.mk
+++ b/v13/Android.mk
@@ -34,6 +34,7 @@
         $(call all-java-files-under, api25) \
         $(call all-java-files-under, java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 # Some projects expect to inherit android-support-v4 from
 # android-support-v13, so we need to keep it static until they can be fixed.
 LOCAL_STATIC_ANDROID_LIBRARIES := \
diff --git a/v13/AndroidManifest-make.xml b/v13/AndroidManifest-make.xml
new file mode 100644
index 0000000..ea25a74
--- /dev/null
+++ b/v13/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.v13">
+    <uses-sdk android:minSdkVersion="13" tools:overrideLibrary="android.support.v13"/>
+    <application />
+</manifest>
diff --git a/v13/AndroidManifest.xml b/v13/AndroidManifest.xml
index ea25a74..7449688 100644
--- a/v13/AndroidManifest.xml
+++ b/v13/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.v13">
     <uses-sdk android:minSdkVersion="13" tools:overrideLibrary="android.support.v13"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}"/>
     <application />
 </manifest>
diff --git a/v13/build.gradle b/v13/build.gradle
index 85aa8a8..fb1c25d 100644
--- a/v13/build.gradle
+++ b/v13/build.gradle
@@ -55,28 +55,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v14/preference/Android.mk b/v14/preference/Android.mk
index 7a0b846..195e8a3 100644
--- a/v14/preference/Android.mk
+++ b/v14/preference/Android.mk
@@ -31,6 +31,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v7-preference \
     android-support-v7-appcompat \
diff --git a/v14/preference/AndroidManifest-make.xml b/v14/preference/AndroidManifest-make.xml
new file mode 100644
index 0000000..b917bb4
--- /dev/null
+++ b/v14/preference/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.support.v14.preference">
+    <uses-sdk android:minSdkVersion="14" />
+    <application />
+</manifest>
diff --git a/v14/preference/AndroidManifest.xml b/v14/preference/AndroidManifest.xml
index 8b502c9..74cff2e 100644
--- a/v14/preference/AndroidManifest.xml
+++ b/v14/preference/AndroidManifest.xml
@@ -1,8 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.support.v14.preference"
-    android:versionCode="1"
-    android:versionName="1.0">
+    package="android.support.v14.preference">
     <uses-sdk android:minSdkVersion="14" />
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v14/preference/build.gradle b/v14/preference/build.gradle
index a7e63d5..5583e93 100644
--- a/v14/preference/build.gradle
+++ b/v14/preference/build.gradle
@@ -14,10 +14,7 @@
  * limitations under the License
  */
 
-
-
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'preference-v14'
 
 dependencies {
@@ -62,28 +59,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v17/leanback/Android.mk b/v17/leanback/Android.mk
index c6a50b4..d91436e 100644
--- a/v17/leanback/Android.mk
+++ b/v17/leanback/Android.mk
@@ -35,6 +35,7 @@
     $(call all-java-files-under, api23) \
     $(call all-java-files-under, src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v7-recyclerview \
     android-support-compat \
diff --git a/v17/leanback/AndroidManifest-make.xml b/v17/leanback/AndroidManifest-make.xml
new file mode 100644
index 0000000..20ef094
--- /dev/null
+++ b/v17/leanback/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v17.leanback">
+    <uses-sdk android:minSdkVersion="17"/>
+    <application />
+</manifest>
diff --git a/v17/leanback/AndroidManifest.xml b/v17/leanback/AndroidManifest.xml
index 20ef094..ded4ce8 100644
--- a/v17/leanback/AndroidManifest.xml
+++ b/v17/leanback/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v17.leanback">
     <uses-sdk android:minSdkVersion="17"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v17/leanback/build.gradle b/v17/leanback/build.gradle
index f5befa7..9ed65a8 100644
--- a/v17/leanback/build.gradle
+++ b/v17/leanback/build.gradle
@@ -64,28 +64,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v17/leanback/generatev4.py b/v17/leanback/generatev4.py
index 2e1eaeb..ae695e50 100755
--- a/v17/leanback/generatev4.py
+++ b/v17/leanback/generatev4.py
@@ -16,6 +16,7 @@
 
 import os
 import sys
+import re
 
 print "Generate v4 fragment related code for leanback"
 
@@ -41,6 +42,7 @@
         line = line.replace('activity.getFragmentManager()', 'activity.getSupportFragmentManager()')
         line = line.replace('Activity activity', 'FragmentActivity activity')
         line = line.replace('(Activity', '(FragmentActivity')
+        line = re.sub(r'FragmentUtil.getContext\(.*this\)', 'getContext()', line);
         outfile.write(line)
     file.close()
     outfile.close()
@@ -86,3 +88,20 @@
     outfile.write(line)
 file.close()
 outfile.close()
+
+print "copy DetailsFragmentBackgroundController to DetailsSupportFragmentBackgroundController".format(w, w)
+file = open('src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java'.format(w), 'r')
+outfile = open('src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java'.format(w), 'w')
+
+outfile.write("// CHECKSTYLE:OFF Generated code\n")
+outfile.write("/* This file is auto-generated from {}DetailsFragmentBackgroundController.java.  DO NOT MODIFY. */\n\n".format(w))
+
+for line in file:
+    line = re.sub(r'FragmentUtil.getContext\(mFragment\)', 'mFragment.getContext()', line);
+    line = line.replace('VideoFragment', 'VideoSupportFragment')
+    line = line.replace('DetailsFragment', 'DetailsSupportFragment')
+    line = line.replace('VideoSupportFragmentGlueHost'.format(w), 'VideoSupportFragmentGlueHost'.format(w))
+    line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
+    outfile.write(line)
+file.close()
+outfile.close()
diff --git a/v17/leanback/res/layout/lb_playback_fragment.xml b/v17/leanback/res/layout/lb_playback_fragment.xml
index 85d52cf..1b0ffa1 100644
--- a/v17/leanback/res/layout/lb_playback_fragment.xml
+++ b/v17/leanback/res/layout/lb_playback_fragment.xml
@@ -18,11 +18,12 @@
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/playback_fragment_root"
         android:layout_width="match_parent"
+        android:transitionGroup="false"
         android:layout_height="match_parent">
 
     <FrameLayout
-            xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/playback_controls_dock"
+            android:transitionGroup="true"
             android:layout_height="match_parent"
             android:layout_width="match_parent"/>
 </FrameLayout>
diff --git a/v17/leanback/res/layout/lb_row_header.xml b/v17/leanback/res/layout/lb_row_header.xml
index 8962e9a..2729ae9 100644
--- a/v17/leanback/res/layout/lb_row_header.xml
+++ b/v17/leanback/res/layout/lb_row_header.xml
@@ -15,14 +15,13 @@
      limitations under the License.
 -->
 
-<android:LinearLayout
+<LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="vertical"
         xmlns:android="http://schemas.android.com/apk/res/android">
 
     <android.support.v17.leanback.widget.RowHeaderView
-            xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/row_header"
             android:importantForAccessibility="no"
             android:layout_width="wrap_content"
@@ -30,11 +29,10 @@
             style="?rowHeaderStyle"/>
 
     <TextView
-            xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/row_header_description"
             android:importantForAccessibility="no"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             style="?rowHeaderDescriptionStyle" />
 
-</android:LinearLayout>
+</LinearLayout>
diff --git a/v17/leanback/res/layout/lb_video_surface.xml b/v17/leanback/res/layout/lb_video_surface.xml
index a3b0fe0..9c6c8fd 100644
--- a/v17/leanback/res/layout/lb_video_surface.xml
+++ b/v17/leanback/res/layout/lb_video_surface.xml
@@ -14,7 +14,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<SurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.v17.leanback.widget.VideoSurfaceView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/video_surface"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />
diff --git a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml b/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
index 3bb7c9f..d535ff4 100644
--- a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
+++ b/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
@@ -25,6 +25,7 @@
           <target android:excludeId="@id/title_text" />
           <target android:excludeId="@id/title_orb" />
           <target android:excludeId="@id/details_background_view" />
+          <target android:excludeId="@id/video_surface" />
       </targets>
   </transition>
   <!-- The ParallaxTransition runs with with Slide transition, must use same duration
@@ -37,5 +38,8 @@
   </transition>
   <fade
       android:duration="350">
+      <targets>
+          <target android:excludeId="@id/video_surface" />
+      </targets>
   </fade>
 </transitionSet>
\ No newline at end of file
diff --git a/v17/leanback/res/transition-v21/lb_details_return_transition.xml b/v17/leanback/res/transition-v21/lb_details_return_transition.xml
index 5e54d2c..82a55f4 100644
--- a/v17/leanback/res/transition-v21/lb_details_return_transition.xml
+++ b/v17/leanback/res/transition-v21/lb_details_return_transition.xml
@@ -24,6 +24,7 @@
           <target android:excludeId="@id/title_text" />
           <target android:excludeId="@id/title_orb" />
           <target android:excludeId="@id/details_background_view" />
+          <target android:excludeId="@id/video_surface" />
       </targets>
   </transition>
     <!-- The ParallaxTransition runs with with Slide transition, must use same duration
@@ -36,5 +37,8 @@
   </transition>
   <fade
       android:duration="350">
+      <targets>
+          <target android:excludeId="@id/video_surface" />
+      </targets>
   </fade>
 </transitionSet>
\ No newline at end of file
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index 870d958..3e2d7f2 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -522,6 +522,14 @@
         <attr name="guidedActionContentWidthNoIcon" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="lbTimePicker">
+        <!-- attr indicating whether time is in 24 hour format (true) or AM/PM format (false). -->
+        <attr name="is24HourFormat" format="boolean" />
+        <!-- attr indicating whether time fields should be initially set to the current time.
+         By default, it's true i.e. TimePicker initializes fields with the current time. -->
+        <attr name="useCurrentTime" format="boolean" />
+    </declare-styleable>
+
     <declare-styleable name="lbDatePicker">
         <attr name="android:minDate" />
         <attr name="android:maxDate" />
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index 4902585..8d174be 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -64,6 +64,7 @@
     <item name="lb_focus_zoom_factor_medium" type="fraction">114%</item>
     <item name="lb_focus_zoom_factor_large" type="fraction">118%</item>
 
+    <dimen name="lb_details_cover_drawable_parallax_movement">50dip</dimen>
     <dimen name="lb_details_overview_height_large">274dp</dimen>
     <dimen name="lb_details_overview_height_small">159dp</dimen>
     <dimen name="lb_details_overview_margin_start">132dp</dimen>
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 3b4c851..8ab731f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
@@ -219,7 +219,7 @@
             @Override
             public boolean onPreDraw() {
                 view.getViewTreeObserver().removeOnPreDrawListener(this);
-                if (getActivity() == null || getView() == null) {
+                if (FragmentUtil.getContext(BaseFragment.this) == null || getView() == null) {
                     // bail out if fragment is destroyed immediately after startEntranceTransition
                     return true;
                 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 98a8f98..d14dc74 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -35,7 +35,7 @@
     private ObjectAdapter mAdapter;
     VerticalGridView mVerticalGridView;
     private PresenterSelector mPresenterSelector;
-    ItemBridgeAdapter mBridgeAdapter;
+    final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
     int mSelectedPosition = -1;
     private boolean mPendingTransitionPrepare;
     private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
@@ -47,8 +47,10 @@
                 @Override
                 public void onChildViewHolderSelected(RecyclerView parent,
                         RecyclerView.ViewHolder view, int position, int subposition) {
-                    mSelectedPosition = position;
-                    onRowSelected(parent, view, position, subposition);
+                    if (!mLateSelectionObserver.mIsLateSelection) {
+                        mSelectedPosition = position;
+                        onRowSelected(parent, view, position, subposition);
+                    }
                 }
             };
 
@@ -77,9 +79,7 @@
         if (savedInstanceState != null) {
             mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
         }
-        if (mBridgeAdapter != null) {
-            setAdapterAndSelection();
-        }
+        setAdapterAndSelection();
         mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
     }
 
@@ -207,7 +207,7 @@
             return;
         }
         mSelectedPosition = position;
-        if(mVerticalGridView != null && mVerticalGridView.getAdapter() != null) {
+        if (mVerticalGridView != null) {
             if (mLateSelectionObserver.mIsLateSelection) {
                 return;
             }
@@ -224,17 +224,9 @@
     }
 
     void updateAdapter() {
-        if (mBridgeAdapter != null) {
-            // detach observer from ObjectAdapter
-            mLateSelectionObserver.clear();
-            mBridgeAdapter.clear();
-            mBridgeAdapter = null;
-        }
+        mBridgeAdapter.setAdapter(mAdapter);
+        mBridgeAdapter.setPresenter(mPresenterSelector);
 
-        if (mAdapter != null) {
-            // If presenter selector is null, adapter ps will be used
-            mBridgeAdapter = new ItemBridgeAdapter(mAdapter, mPresenterSelector);
-        }
         if (mVerticalGridView != null) {
             setAdapterAndSelection();
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
index 281c493..d36f68f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -38,7 +38,7 @@
     private ObjectAdapter mAdapter;
     VerticalGridView mVerticalGridView;
     private PresenterSelector mPresenterSelector;
-    ItemBridgeAdapter mBridgeAdapter;
+    final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
     int mSelectedPosition = -1;
     private boolean mPendingTransitionPrepare;
     private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
@@ -50,8 +50,10 @@
                 @Override
                 public void onChildViewHolderSelected(RecyclerView parent,
                         RecyclerView.ViewHolder view, int position, int subposition) {
-                    mSelectedPosition = position;
-                    onRowSelected(parent, view, position, subposition);
+                    if (!mLateSelectionObserver.mIsLateSelection) {
+                        mSelectedPosition = position;
+                        onRowSelected(parent, view, position, subposition);
+                    }
                 }
             };
 
@@ -80,9 +82,7 @@
         if (savedInstanceState != null) {
             mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
         }
-        if (mBridgeAdapter != null) {
-            setAdapterAndSelection();
-        }
+        setAdapterAndSelection();
         mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
     }
 
@@ -210,7 +210,7 @@
             return;
         }
         mSelectedPosition = position;
-        if(mVerticalGridView != null && mVerticalGridView.getAdapter() != null) {
+        if (mVerticalGridView != null) {
             if (mLateSelectionObserver.mIsLateSelection) {
                 return;
             }
@@ -227,17 +227,9 @@
     }
 
     void updateAdapter() {
-        if (mBridgeAdapter != null) {
-            // detach observer from ObjectAdapter
-            mLateSelectionObserver.clear();
-            mBridgeAdapter.clear();
-            mBridgeAdapter = null;
-        }
+        mBridgeAdapter.setAdapter(mAdapter);
+        mBridgeAdapter.setPresenter(mPresenterSelector);
 
-        if (mAdapter != null) {
-            // If presenter selector is null, adapter ps will be used
-            mBridgeAdapter = new ItemBridgeAdapter(mAdapter, mPresenterSelector);
-        }
         if (mVerticalGridView != null) {
             setAdapterAndSelection();
         }
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 8de54a7..7d08738 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
@@ -222,7 +222,7 @@
             @Override
             public boolean onPreDraw() {
                 view.getViewTreeObserver().removeOnPreDrawListener(this);
-                if (getActivity() == null || getView() == null) {
+                if (getContext() == null || getView() == null) {
                     // bail out if fragment is destroyed immediately after startEntranceTransition
                     return true;
                 }
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 951b8a8..1c9e5ca 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -19,6 +19,7 @@
 import android.app.FragmentManager;
 import android.app.FragmentManager.BackStackEntry;
 import android.app.FragmentTransaction;
+import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -1056,12 +1057,13 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
+        final Context context = FragmentUtil.getContext(this);
+        TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
         mContainerListMarginStart = (int) ta.getDimension(
-                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
+                R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
                 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
         mContainerListAlignTop = (int) ta.getDimension(
-                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
+                R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
                 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
         ta.recycle();
 
@@ -1100,12 +1102,23 @@
         super.onDestroy();
     }
 
+    /**
+     * Creates a new {@link HeadersFragment} instance. Subclass of BrowseFragment may override and
+     * return an instance of subclass of HeadersFragment, e.g. when app wants to replace presenter
+     * to render HeaderItem.
+     *
+     * @return A new instance of {@link HeadersFragment} or its subclass.
+     */
+    public HeadersFragment onCreateHeadersFragment() {
+        return new HeadersFragment();
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
 
         if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
-            mHeadersFragment = new HeadersFragment();
+            mHeadersFragment = onCreateHeadersFragment();
 
             createMainFragment(mAdapter, mSelectedPosition);
             FragmentTransaction ft = getChildFragmentManager().beginTransaction()
@@ -1223,7 +1236,7 @@
     }
 
     void createHeadersTransition() {
-        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
+        mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(this),
                 mShowingHeaders
                         ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
 
@@ -1657,7 +1670,7 @@
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(FragmentUtil.getContext(this),
                 R.transition.lb_browse_entrance_transition);
     }
 
@@ -1740,7 +1753,7 @@
 
         @Override
         public boolean onPreDraw() {
-            if (getView() == null || getActivity() == null) {
+            if (getView() == null || FragmentUtil.getContext(BrowseFragment.this) == null) {
                 mView.getViewTreeObserver().removeOnPreDrawListener(this);
                 return true;
             }
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 0ff34f0..c7d8de4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -22,6 +22,7 @@
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentManager.BackStackEntry;
 import android.support.v4.app.FragmentTransaction;
+import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -1059,12 +1060,13 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
+        final Context context = getContext();
+        TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
         mContainerListMarginStart = (int) ta.getDimension(
-                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
+                R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
                 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
         mContainerListAlignTop = (int) ta.getDimension(
-                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
+                R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
                 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
         ta.recycle();
 
@@ -1103,12 +1105,23 @@
         super.onDestroy();
     }
 
+    /**
+     * Creates a new {@link HeadersSupportFragment} instance. Subclass of BrowseSupportFragment may override and
+     * return an instance of subclass of HeadersSupportFragment, e.g. when app wants to replace presenter
+     * to render HeaderItem.
+     *
+     * @return A new instance of {@link HeadersSupportFragment} or its subclass.
+     */
+    public HeadersSupportFragment onCreateHeadersSupportFragment() {
+        return new HeadersSupportFragment();
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
 
         if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
-            mHeadersSupportFragment = new HeadersSupportFragment();
+            mHeadersSupportFragment = onCreateHeadersSupportFragment();
 
             createMainFragment(mAdapter, mSelectedPosition);
             FragmentTransaction ft = getChildFragmentManager().beginTransaction()
@@ -1226,7 +1239,7 @@
     }
 
     void createHeadersTransition() {
-        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
+        mHeadersTransition = TransitionHelper.loadTransition(getContext(),
                 mShowingHeaders
                         ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
 
@@ -1660,7 +1673,7 @@
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(getContext(),
                 R.transition.lb_browse_entrance_transition);
     }
 
@@ -1743,7 +1756,7 @@
 
         @Override
         public boolean onPreDraw() {
-            if (getView() == null || getActivity() == null) {
+            if (getView() == null || getContext() == null) {
                 mView.getViewTreeObserver().removeOnPreDrawListener(this);
                 return true;
             }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundParallaxHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundParallaxHelper.java
deleted file mode 100644
index f63b3f3..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundParallaxHelper.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.graphics.BoundsRule;
-import android.support.v17.leanback.graphics.CompositeDrawable;
-import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
-import android.support.v17.leanback.widget.Parallax;
-import android.support.v17.leanback.widget.ParallaxRecyclerViewSource;
-import android.util.TypedValue;
-
-/**
- * Helper class responsible for wiring in parallax effect in
- * {@link android.support.v17.leanback.app.DetailsFragment}. The default effect will render
- * a drawable like the following -
- * <pre>
- *        ***************************
- *        *        Cover Image      *
- *        ***************************
- *        *    DetailsOverviewRow   *
- *        *                         *
- *        ***************************
- *        *        Solid Color      *
- *        *         Related         *
- *        *         Content         *
- *        ***************************
- * </pre>
- * As the user scrolls through the page, the bounds of the bitmap and related content section
- * will be updated to simulate the parallax effect. Users have to do the following to setup the
- * parallax -
- *
- * <ul>
- * <li>First users should use {@link ParallaxBuilder} class to set the appropriate attributes
- * and call build() to create an instance of {@link DetailsBackgroundParallaxHelper}.
- * Users must set {@link DetailsParallaxManager} on {@link ParallaxBuilder} for it to obtain the
- * {@link Parallax} instance. Finally they should set the drawable obtained by calling
- * {@link #getDrawable} as the background of their current activity.
- * <pre>
- * {@code
- *     public void onStart() {
- *         super.onStart();
- *         mParallaxHelper = DetailsBackgroundParallaxHelper.ParallaxBuilder
- *             .newBuilder(parallaxManager, context)
- *             .setCoverImageMinVerticalOffset(-300)
- *             .build();
- *          mBackgroundManager.setDrawable(mParallaxHelper.getDrawable());
- *      }
- * }
- * </pre>
- * </li>
- * </li>
- * <li>Finally, users can set the bitmap through {@link #setCoverImageBitmap(Bitmap)} call.
- * <pre>
- * {@code
- *     public void onBitmapLoaded(Bitmap bitmap) {
- *         mParallaxHelper.setCoverImageBitmap(bitmap);
- *     }
- * }
- * </pre>
- * </li>
- * </ul>
- *
- * In case the color is not set, it will use defaultBrandColorDark from LeanbackTheme.
- * @hide
- */
-public final class DetailsBackgroundParallaxHelper {
-    private DetailsParallaxManager mDetailsParallaxManager;
-    private CompositeDrawable mCompositeDrawable;
-    private FitWidthBitmapDrawable mCoverImageDrawable;
-    private ColorDrawable mSolidColorDrawable;
-    private int mCoverImageMinVerticalOffset;
-
-    DetailsBackgroundParallaxHelper(
-            Context context,
-            DetailsParallaxManager detailsParallaxManager,
-            int coverImageMinVerticalOffset,
-            int color) {
-        this.mCoverImageMinVerticalOffset = coverImageMinVerticalOffset;
-        mCompositeDrawable = new CompositeDrawable();
-        mCoverImageDrawable = new FitWidthBitmapDrawable();
-        mSolidColorDrawable = new ColorDrawable(color);
-        mCompositeDrawable.addChildDrawable(mCoverImageDrawable);
-        mCompositeDrawable.addChildDrawable(mSolidColorDrawable);
-        mCompositeDrawable.getChildAt(0).getBoundsRule().mBottom = BoundsRule.inheritFromParent(1f);
-        mCompositeDrawable.getChildAt(1).getBoundsRule().mTop = BoundsRule.inheritFromParent(1f);
-        mDetailsParallaxManager = detailsParallaxManager;
-        setupParallaxEffect(context);
-    }
-
-    /**
-     * Returns the first child of {@link CompositeDrawable} which is the cover image.
-     */
-    public Drawable getCoverImageDrawable() {
-        return mCompositeDrawable.getChildAt(0).getDrawable();
-    }
-
-    /**
-     * Builder class used for creating an instance of {@link DetailsBackgroundParallaxHelper}.
-     */
-    public static class ParallaxBuilder {
-        // Default value for image translation is -100px.
-        private int mCoverImageMinVerticalOffset = -100;
-        private int mColor;
-        private boolean mIsColorSet;
-        private final DetailsParallaxManager mDetailsParallaxManager;
-        private final Context mContext;
-
-        /**
-         * Returns an instance of itself.
-         *
-         * @param detailsParallaxManager class responsible for creating {@link Parallax} instance.
-         * @param context Context used for loading resources.
-         */
-        public ParallaxBuilder(@NonNull Context context,
-                               @NonNull DetailsParallaxManager detailsParallaxManager) {
-            if (detailsParallaxManager == null || context == null) {
-                throw new IllegalArgumentException("Must set DetailsParallaxManager and Context.");
-            }
-            this.mDetailsParallaxManager = detailsParallaxManager;
-            this.mContext = context;
-        }
-
-        /**
-         * Sets the minimum top position the image is going to translate to during the
-         * parallax motion.
-         */
-        public ParallaxBuilder setCoverImageMinVerticalOffset(int minTop) {
-            this.mCoverImageMinVerticalOffset = minTop;
-            return this;
-        }
-
-        /**
-         * Sets the color for the bottom section of the
-         * {@link android.support.v17.leanback.app.DetailsFragment}.
-         */
-        public ParallaxBuilder setColor(int color) {
-            this.mColor = color;
-            mIsColorSet = true;
-            return this;
-        }
-
-        /**
-         * Builds and returns an instance of {@link DetailsBackgroundParallaxHelper}.
-         */
-        public DetailsBackgroundParallaxHelper build() {
-            if (!mIsColorSet) {
-                mColor = getDefaultBackgroundColor(mContext);
-            }
-
-            return new DetailsBackgroundParallaxHelper(mContext,
-                    mDetailsParallaxManager, mCoverImageMinVerticalOffset, mColor);
-        }
-
-        private int getDefaultBackgroundColor(Context context) {
-            TypedValue outValue = new TypedValue();
-            if (context.getTheme().resolveAttribute(R.attr.defaultBrandColorDark, outValue, true)) {
-                return context.getResources().getColor(outValue.resourceId);
-            }
-            return context.getResources().getColor(R.color.lb_default_brand_color_dark);
-        }
-    }
-
-    /**
-     * Returns the special drawable instance that is used to simulate the parallax effect. Users
-     * must set this drawable as the background for their activity.
-     */
-    public Drawable getDrawable() {
-        return mCompositeDrawable;
-    }
-
-    /**
-     * Sets the bitmap in drawable instance returned during {@link #getDrawable()} call.
-     */
-    public void setCoverImageBitmap(Bitmap bitmap) {
-        if (bitmap == null) {
-            throw new IllegalArgumentException("Invalid bitmap");
-        }
-        mCoverImageDrawable.setBitmap(bitmap);
-    }
-
-    /**
-     * Changes the background color of the related content section.
-     */
-    public void setColor(@ColorInt int color) {
-        mSolidColorDrawable.setColor(color);
-    }
-
-    /**
-     * Sets up the cover image parallax effect in {@link DetailsFragment}.
-     */
-    private void setupParallaxEffect(Context context) {
-        // Add cover image parallax effect:
-        // When frameTop moves from half of the screen to top of the screen,
-        // change vertical offset of Bitmap from 0 to -100
-
-        Parallax parallax = mDetailsParallaxManager.getParallax();
-        ParallaxRecyclerViewSource.ChildPositionProperty frameTop =
-                mDetailsParallaxManager.getFrameTop();
-        ParallaxRecyclerViewSource.ChildPositionProperty frameBottom =
-                mDetailsParallaxManager.getFrameBottom();
-        // The values are from DetailsFragment.setupDetailsOverviewRowPresenter()
-        final int fromValue = context.getResources()
-                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions);
-        final int toValue = context.getResources()
-                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description);
-        parallax.addEffect(frameTop.atAbsolute(fromValue), frameTop.atFraction(toValue))
-                .target(mCoverImageDrawable,
-                    PropertyValuesHolder.ofInt("verticalOffset", 0, mCoverImageMinVerticalOffset));
-
-        // Add solid color parallax effect:
-        // When frameBottom moves from bottom of the screen to top of the screen,
-        // change solid ColorDrawable's top from bottom of screen to top of the screen.
-        parallax.addEffect(frameBottom.atFraction(1f), frameBottom.atFraction(0f))
-                .target(mCompositeDrawable.getChildAt(1),
-                        PropertyValuesHolder.ofFloat(
-                                CompositeDrawable.ChildDrawable.TOP_FRACTION, 1f, 0f));
-        // Also when frameTop moves from bottom of screen to top of the screen,
-        // we are changing bottom of the bitmap from bottom of screen to top of screen.
-        parallax.addEffect(frameTop.atFraction(1f), frameTop.atFraction(0f))
-                .target(mCompositeDrawable.getChildAt(0),
-                        PropertyValuesHolder.ofFloat(
-                                CompositeDrawable.ChildDrawable.BOTTOM_FRACTION, 1f, 0f));
-    }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
new file mode 100644
index 0000000..37a6bfc
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -0,0 +1,200 @@
+/*
+ * 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.app;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.widget.DetailsParallax;
+import android.support.v17.leanback.widget.Parallax;
+import android.support.v17.leanback.widget.ParallaxEffect;
+import android.support.v17.leanback.widget.ParallaxTarget;
+
+/**
+ * Helper class responsible for controlling video playback in {@link DetailsFragment}. This
+ * takes {@link DetailsParallax}, {@link PlaybackGlue} and a drawable as input.
+ * Video is played when {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of screen.
+ * Video is stopped when {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above top
+ * edge of screen. The drawable will change alpha to 0 when video is ready to play.
+ * App does not directly use this class.
+ * @see DetailsFragmentBackgroundController
+ * @see DetailsSupportFragmentBackgroundController
+ */
+final class DetailsBackgroundVideoHelper {
+    private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
+    // Temporarily add CROSSFADE_DELAY waiting for video surface ready.
+    // We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.
+    private static final long CROSSFADE_DELAY = 1000;
+
+    /**
+     * Different states {@link DetailsFragment} can be in.
+     */
+    static final int INITIAL = 0;
+    static final int PLAY_VIDEO = 1;
+    static final int NO_VIDEO = 2;
+
+    private final DetailsParallax mDetailsParallax;
+    private ParallaxEffect mParallaxEffect;
+
+    private int mCurrentState = INITIAL;
+
+    private ValueAnimator mBackgroundAnimator;
+    private Drawable mBackgroundDrawable;
+    private PlaybackGlue mPlaybackGlue;
+
+    /**
+     * Constructor to setup a Helper for controlling video playback in DetailsFragment.
+     * @param playbackGlue The PlaybackGlue used to control underlying player.
+     * @param detailsParallax The DetailsParallax to add special parallax effect to control video
+     *                        start/stop. Video is played when
+     *                        {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of
+     *                        screen. Video is stopped when
+     *                        {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above
+     *                        top edge of screen.
+     * @param backgroundDrawable The drawable will change alpha to 0 when video is ready to play.
+     */
+    DetailsBackgroundVideoHelper(
+            PlaybackGlue playbackGlue,
+            DetailsParallax detailsParallax,
+            Drawable backgroundDrawable) {
+        this.mPlaybackGlue = playbackGlue;
+        this.mDetailsParallax = detailsParallax;
+        this.mBackgroundDrawable = backgroundDrawable;
+        startParallax();
+    }
+
+    void startParallax() {
+        if (mParallaxEffect != null) {
+            return;
+        }
+        Parallax.IntProperty frameTop = mDetailsParallax.getOverviewRowTop();
+        final float maxFrameTop = 1f;
+        final float minFrameTop = 0f;
+        mParallaxEffect = mDetailsParallax
+                .addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))
+                .target(new ParallaxTarget() {
+
+                    float mFraction;
+                    @Override
+                    public void update(float fraction) {
+                        if (fraction == maxFrameTop) {
+                            updateState(NO_VIDEO);
+                        } else {
+                            updateState(PLAY_VIDEO);
+                        }
+                        mFraction = fraction;
+                    }
+
+                    @Override
+                    public float getFraction() {
+                        return mFraction;
+                    }
+                });
+    }
+
+    void stopParallax() {
+        mDetailsParallax.removeEffect(mParallaxEffect);
+    }
+
+    boolean isVideoVisible() {
+        return mCurrentState == PLAY_VIDEO;
+    }
+
+    private void updateState(int state) {
+        if (state == mCurrentState) {
+            return;
+        }
+        mCurrentState = state;
+        switch (state) {
+            case PLAY_VIDEO:
+                if (mPlaybackGlue.isReadyForPlayback()) {
+                    internalStartPlayback();
+                } else {
+                    mPlaybackGlue.setPlayerCallback(new PlaybackControlStateCallback());
+                }
+                break;
+            case NO_VIDEO:
+                crossFadeBackgroundToVideo(false);
+                mPlaybackGlue.setPlayerCallback(null);
+                mPlaybackGlue.pause();
+                break;
+        }
+    }
+
+    private void internalStartPlayback() {
+        mPlaybackGlue.play();
+        mDetailsParallax.getRecyclerView().postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                crossFadeBackgroundToVideo(true);
+            }
+        }, CROSSFADE_DELAY);
+    }
+
+    private void crossFadeBackgroundToVideo(final boolean crossFadeToVideo) {
+        if (mBackgroundAnimator != null) {
+            mBackgroundAnimator.cancel();
+            mBackgroundAnimator = null;
+        }
+
+        float startAlpha = crossFadeToVideo ? 1f : 0f;
+        float endAlpha = crossFadeToVideo ? 0f : 1f;
+
+        if (mBackgroundDrawable == null) {
+            return;
+        }
+        mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
+        mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
+        mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mBackgroundDrawable.setAlpha(
+                        (int) ((Float) (valueAnimator.getAnimatedValue()) * 255));
+            }
+        });
+
+        mBackgroundAnimator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                mBackgroundAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animator) {
+            }
+        });
+
+        mBackgroundAnimator.start();
+    }
+
+    private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {
+
+        @Override
+        public void onReadyForPlayback() {
+            internalStartPlayback();
+        }
+    }
+}
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 fcb3fa9..a2480d4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -13,16 +13,21 @@
  */
 package android.support.v17.leanback.app;
 
+import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentTransaction;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.CallSuper;
 import android.support.v17.leanback.R;
-import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
 import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.DetailsParallax;
 import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
 import android.support.v17.leanback.widget.ItemAlignmentFacet;
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
@@ -37,6 +42,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import java.lang.ref.WeakReference;
+
 /**
  * A fragment for creating Leanback details screens.
  *
@@ -75,6 +82,23 @@
     static final String TAG = "DetailsFragment";
     static boolean DEBUG = false;
 
+    /**
+     * Flag for "possibly" having enter transition not finished yet.
+     * @see #mStartAndTransitionFlag
+     */
+    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;
+
     private class SetSelectionRunnable implements Runnable {
         int mPosition;
         boolean mSmooth = true;
@@ -91,14 +115,91 @@
         }
     }
 
+    /**
+     * 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) {
+            if (mWaitEnterTransitionTimeout != null) {
+                // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
+                // when transition finishes.
+                mWaitEnterTransitionTimeout.mRef.clear();
+            }
+        }
+
+        @Override
+        public void onTransitionCancel(Object transition) {
+            clearPendingEnterTransition();
+        }
+
+        @Override
+        public void onTransitionEnd(Object transition) {
+            clearPendingEnterTransition();
+        }
+    };
+
+    TransitionListener mReturnTransitionListener = new TransitionListener() {
+        @Override
+        public void onTransitionStart(Object transition) {
+            onReturnTransitionStart();
+        }
+    };
+
     BrowseFrameLayout mRootView;
+    View mBackgroundView;
+    Drawable mBackgroundDrawable;
     Fragment mVideoFragment;
-    DetailsParallaxManager mDetailsParallaxManager;
+    DetailsParallax mDetailsParallax;
     RowsFragment mRowsFragment;
     ObjectAdapter mAdapter;
     int mContainerListAlignTop;
     BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener;
     BaseOnItemViewClickedListener mOnItemViewClickedListener;
+    DetailsFragmentBackgroundController mDetailsBackgroundController;
+
+
+    /**
+     * 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;
 
@@ -177,6 +278,19 @@
         super.onCreate(savedInstanceState);
         mContainerListAlignTop =
             getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+
+        Activity activity = getActivity();
+        if (activity != null) {
+            Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
+            if (transition != null) {
+                mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
+                TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+            }
+            transition = TransitionHelper.getReturnTransition(activity.getWindow());
+            if (transition != null) {
+                TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
+            }
+        }
     }
 
     @Override
@@ -184,6 +298,10 @@
             Bundle savedInstanceState) {
         mRootView = (BrowseFrameLayout) inflater.inflate(
                 R.layout.lb_details_fragment, container, false);
+        mBackgroundView = mRootView.findViewById(R.id.details_background_view);
+        if (mBackgroundView != null) {
+            mBackgroundView.setBackground(mBackgroundDrawable);
+        }
         mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
                 R.id.details_rows_dock);
         if (mRowsFragment == null) {
@@ -210,13 +328,13 @@
             mRowsFragment.setExternalAdapterListener(new ItemBridgeAdapter.AdapterListener() {
                 @Override
                 public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
-                    if (mDetailsParallaxManager != null && vh.getViewHolder()
+                    if (mDetailsParallax != null && vh.getViewHolder()
                             instanceof FullWidthDetailsOverviewRowPresenter.ViewHolder) {
                         FullWidthDetailsOverviewRowPresenter.ViewHolder rowVh =
                                 (FullWidthDetailsOverviewRowPresenter.ViewHolder)
                                         vh.getViewHolder();
                         rowVh.getOverviewView().setTag(R.id.lb_parallax_source,
-                                mDetailsParallaxManager.getParallax().getSource());
+                                mDetailsParallax);
                     }
                 }
             });
@@ -327,56 +445,26 @@
     }
 
     /**
-     * Creates an instance of {@link VideoFragment}. Subclasses can override this method
-     * and provide their own instance of a {@link Fragment}. When you provide your own instance of
-     * video fragment, you MUST also provide a custom
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost}.
-     * @hide
-     */
-    public Fragment onCreateVideoFragment() {
-        return new VideoFragment();
-    }
-
-    /**
-     * Creates an instance of
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost}. The implementation
-     * of this host depends on the instance of video fragment {@link #onCreateVideoFragment()}.
-     * @hide
-     */
-    public PlaybackGlueHost onCreateVideoFragmentHost(Fragment fragment) {
-        return new VideoFragmentGlueHost((VideoFragment) fragment);
-    }
-
-    /**
-     * This method adds a fragment for rendering video to the layout. In case the
-     * fragment is being restored, it will return the video fragment in there.
+     * 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
+     * called after calling super.onCreate(). App usually does not call this method directly.
      *
      * @return Fragment the added or restored fragment responsible for rendering video.
-     * @hide
+     * @see DetailsFragmentBackgroundController#onCreateVideoFragment()
      */
-    public final Fragment findOrCreateVideoFragment() {
-        Fragment fragment = getFragmentManager().findFragmentById(R.id.video_surface_container);
-        if (fragment == null) {
-            FragmentTransaction ft2 = getFragmentManager().beginTransaction();
+    final Fragment findOrCreateVideoFragment() {
+        Fragment fragment = getChildFragmentManager()
+                .findFragmentById(R.id.video_surface_container);
+        if (fragment == null && mDetailsBackgroundController != null) {
+            FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
             ft2.add(android.support.v17.leanback.R.id.video_surface_container,
-                    fragment = onCreateVideoFragment());
+                    fragment = mDetailsBackgroundController.onCreateVideoFragment());
             ft2.commit();
         }
         mVideoFragment = fragment;
         return mVideoFragment;
     }
 
-    /**
-     * This method initializes a video fragment, create an instance of
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost} using that fragment
-     * and return it.
-     * @hide
-     */
-    public final PlaybackGlueHost createPlaybackGlueHost() {
-        Fragment fragment = findOrCreateVideoFragment();
-        return onCreateVideoFragmentHost(fragment);
-    }
-
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
         if (( mRowsFragment != null && mRowsFragment.getView() != null
@@ -391,6 +479,11 @@
         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);
+                }
+            }
             for (int i = 0; i < count; i++) {
                 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
                         gridView.getChildViewHolder(gridView.getChildAt(i));
@@ -403,6 +496,60 @@
         }
     }
 
+    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.
+     */
+    @CallSuper
+    void onSafeStart() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStart();
+        }
+    }
+
+    @CallSuper
+    void onReturnTransitionStart() {
+        if (mDetailsBackgroundController != null) {
+            // first disable parallax effect that auto-start PlaybackGlue.
+            boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
+            // if video is not visible we can safely remove VideoFragment,
+            // otherwise let video playing during return transition.
+            if (!isVideoVisible && mVideoFragment != null) {
+                FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+                ft2.remove(mVideoFragment);
+                ft2.commit();
+                mVideoFragment = null;
+            }
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStop();
+        }
+        super.onStop();
+    }
+
     /**
      * Called on every visible row to change view status when current selected row position
      * or selected sub position changed.  Subclass may override.   The default
@@ -461,19 +608,23 @@
     @Override
     public void onStart() {
         super.onStart();
+
+        mStartAndTransitionFlag |= PF_PENDING_START;
+        dispatchOnStartAndTransitionFinished();
+
         setupChildFragmentLayout();
         if (isEntranceTransitionEnabled()) {
             mRowsFragment.setEntranceTransitionState(false);
         }
-        if (mDetailsParallaxManager != null) {
-            mDetailsParallaxManager.setRecyclerView(mRowsFragment.getVerticalGridView());
+        if (mDetailsParallax != null) {
+            mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
         }
         mRowsFragment.getVerticalGridView().requestFocus();
     }
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(FragmentUtil.getContext(this),
                 R.transition.lb_details_enter_transition);
     }
 
@@ -484,11 +635,14 @@
 
     @Override
     protected void onEntranceTransitionEnd() {
+        mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
+        dispatchOnStartAndTransitionFinished();
         mRowsFragment.onTransitionEnd();
     }
 
     @Override
     protected void onEntranceTransitionPrepare() {
+        mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
         mRowsFragment.onTransitionPrepare();
     }
 
@@ -498,44 +652,34 @@
     }
 
     /**
-     * Create a DetailsParallaxManager that will be used to configure parallax effect of background
-     * and start/stop Video playback. Subclass may override.
+     * Returns the {@link DetailsParallax} instance used by
+     * {@link DetailsFragmentBackgroundController} to configure parallax effect of background and
+     * control embedded video playback. App usually does not use this method directly.
+     * App may use this method for other custom parallax tasks.
      *
-     * @return The new created DetailsParallaxManager.
-     * @see #getParallaxManager()
-     * @hide
+     * @return The DetailsParallax instance attached to the DetailsFragment.
      */
-    public DetailsParallaxManager onCreateParallaxManager() {
-        return new DetailsParallaxManager();
-    }
-
-    /**
-     * Returns the {@link DetailsParallaxManager} instance used to configure parallax effect of
-     * background.
-     *
-     * @return The DetailsParallaxManager instance attached to the DetailsFragment.
-     * @see #onCreateParallaxManager()
-     * @hide
-     */
-    public DetailsParallaxManager getParallaxManager() {
-        if (mDetailsParallaxManager == null) {
-            mDetailsParallaxManager = onCreateParallaxManager();
+    public DetailsParallax getParallax() {
+        if (mDetailsParallax == null) {
+            mDetailsParallax = new DetailsParallax();
             if (mRowsFragment != null && mRowsFragment.getView() != null) {
-                mDetailsParallaxManager.setRecyclerView(mRowsFragment.getVerticalGridView());
+                mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
             }
         }
-        return mDetailsParallaxManager;
+        return mDetailsParallax;
     }
 
     /**
-     * Returns background View that above VideoFragment. App can set a background drawable to this
-     * view to hide the VideoFragment before it is ready to play.
+     * Set background drawable shown below foreground rows UI and above
+     * {@link #findOrCreateVideoFragment()}.
      *
-     * @see #findOrCreateVideoFragment()
-     * @hide
+     * @see DetailsFragmentBackgroundController
      */
-    public View getBackgroundView() {
-        return mRootView == null ? null : mRootView.findViewById(R.id.details_background_view);
+    void setBackgroundDrawable(Drawable drawable) {
+        if (mBackgroundView != null) {
+            mBackgroundView.setBackground(drawable);
+        }
+        mBackgroundDrawable = drawable;
     }
 
     /**
@@ -549,6 +693,27 @@
      * </ul>
      */
     void setupDpadNavigation() {
+        mRootView.setOnChildFocusListener(new BrowseFrameLayout.OnChildFocusListener() {
+
+            @Override
+            public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+                return false;
+            }
+
+            @Override
+            public void onRequestChildFocus(View child, View focused) {
+                if (child != mRootView.getFocusedChild()) {
+                    if (child.getId() == R.id.details_fragment_root) {
+                        showTitle(true);
+                    } else if (child.getId() == R.id.video_surface_container) {
+                        slideOutGridView();
+                        showTitle(false);
+                    } else {
+                        showTitle(true);
+                    }
+                }
+            }
+        });
         mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
             @Override
             public View onFocusSearch(View focused, int direction) {
@@ -556,10 +721,8 @@
                         && mRowsFragment.getVerticalGridView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
                         if (mVideoFragment != null && mVideoFragment.getView() != null) {
-                            slideOutGridView();
-                            showTitle(false);
                             return mVideoFragment.getView();
-                        } else if (getTitleView() != null) {
+                        } else if (getTitleView() != null && getTitleView().hasFocusable()) {
                             return getTitleView();
                         }
                     }
@@ -567,8 +730,6 @@
                         && mVideoFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_DOWN) {
                         if (mRowsFragment.getVerticalGridView() != null) {
-                            showTitle(true);
-                            slideInGridView();
                             return mRowsFragment.getVerticalGridView();
                         }
                     }
@@ -583,7 +744,7 @@
             }
         });
 
-        // If we press BACK or DOWN on remote while in full screen video mode, we should
+        // If we press BACK on remote while in full screen video mode, we should
         // transition back to half screen video playback mode.
         mRootView.setOnDispatchKeyListener(new View.OnKeyListener() {
             @Override
@@ -593,9 +754,7 @@
                 // focusability of the video surface view.
                 if (mVideoFragment != null && mVideoFragment.getView() != null
                         && mVideoFragment.getView().hasFocus()) {
-                    if (keyCode == KeyEvent.KEYCODE_BACK) {
-                        showTitle(true);
-                        slideInGridView();
+                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
                         getVerticalGridView().requestFocus();
                         return true;
                     }
@@ -610,13 +769,9 @@
      * Slides vertical grid view (displaying media item details) out of the screen from below.
      */
     void slideOutGridView() {
-        getVerticalGridView().animateOut();
+        if (getVerticalGridView() != null) {
+            getVerticalGridView().animateOut();
+        }
     }
 
-    /**
-     * Slides in vertical grid view (displaying media item details) from below.
-     */
-    void slideInGridView() {
-        getVerticalGridView().animateIn();
-    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
new file mode 100644
index 0000000..ef09ae6
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -0,0 +1,403 @@
+/*
+ * 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.app;
+
+import android.animation.PropertyValuesHolder;
+import android.app.Fragment;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+
+/**
+ * Controller for DetailsFragment parallax background and embedded video play.
+ * <p>
+ * The parallax background drawable is made of two parts: cover drawable (by default
+ * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default
+ * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size
+ * of cover drawable and bottom drawable will be updated and the cover drawable will by default
+ * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.
+ * </p>
+ * <pre>
+ *        ***************************
+ *        *      Cover Drawable     *
+ *        * (FitWidthBitmapDrawable)*
+ *        *                         *
+ *        ***************************
+ *        *    DetailsOverviewRow   *
+ *        *                         *
+ *        ***************************
+ *        *     Bottom Drawable     *
+ *        *      (ColorDrawable)    *
+ *        *         Related         *
+ *        *         Content         *
+ *        ***************************
+ * </pre>
+ * Both parallax background drawable and embedded video play are optional. App must call
+ * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.
+ * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and
+ * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable
+ * will be faded out.
+ * Example:
+ * <pre>
+ * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(this);
+ *
+ * public void onCreate(Bundle savedInstance) {
+ *     super.onCreate(savedInstance);
+ *     MediaPlayerGlue player = new MediaPlayerGlue(..);
+ *     player.setUrl(...);
+ *     mController.enableParallax();
+ *     mController.setupVideoPlayback(player);
+ * }
+ *
+ * static class MyLoadBitmapTask extends ... {
+ *     WeakReference<MyFragment> mFragmentRef;
+ *     MyLoadBitmapTask(MyFragment fragment) {
+ *         mFragmentRef = new WeakReference(fragment);
+ *     }
+ *     protected void onPostExecute(Bitmap bitmap) {
+ *         MyFragment fragment = mFragmentRef.get();
+ *         if (fragment != null) {
+ *             fragment.mController.setCoverBitmap(bitmap);
+ *         }
+ *     }
+ * }
+ *
+ * public void onStart() {
+ *     new MyLoadBitmapTask(this).execute(url);
+ * }
+ *
+ * public void onStop() {
+ *     mController.setCoverBitmap(null);
+ * }
+ * </pre>
+ * <p>
+ * To customize cover drawable and/or bottom drawable, app should call
+ * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.
+ * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.
+ * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.
+ * </p>
+ * <p>
+ * To customize playback fragment, app should override {@link #onCreateVideoFragment()} and
+ * {@link #onCreateGlueHost()}.
+ * </p>
+ *
+ */
+public class DetailsFragmentBackgroundController {
+
+    private final DetailsFragment mFragment;
+    private DetailsParallaxDrawable mParallaxDrawable;
+    private int mParallaxDrawableMaxOffset;
+    private PlaybackGlue mPlaybackGlue;
+    private DetailsBackgroundVideoHelper mVideoHelper;
+    private Bitmap mCoverBitmap;
+    private int mSolidColor;
+    private boolean mCanUseHost = false;
+
+    /**
+     * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that
+     * each DetailsFragment can only associate with one DetailsFragmentBackgroundController.
+     *
+     * @param fragment The DetailsFragment to control background and embedded video playing.
+     * @throws IllegalStateException If fragment was already associated with another controller.
+     */
+    public DetailsFragmentBackgroundController(DetailsFragment fragment) {
+        if (fragment.mDetailsBackgroundController != null) {
+            throw new IllegalStateException("Each DetailsFragment is allowed to initialize "
+                    + "DetailsFragmentBackgroundController once");
+        }
+        fragment.mDetailsBackgroundController = this;
+        mFragment = fragment;
+    }
+
+    /**
+     * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable
+     * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied
+     * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and
+     * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.
+     * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+     *
+     * @see #setCoverBitmap(Bitmap)
+     * @see #setSolidColor(int)
+     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+     */
+    public void enableParallax() {
+        int offset = mParallaxDrawableMaxOffset;
+        if (offset == 0) {
+            offset = FragmentUtil.getContext(mFragment).getResources()
+                    .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);
+        }
+        Drawable coverDrawable = new FitWidthBitmapDrawable();
+        ColorDrawable colorDrawable = new ColorDrawable();
+        enableParallax(coverDrawable, colorDrawable,
+                new ParallaxTarget.PropertyValuesHolderTarget(
+                        coverDrawable,
+                        PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+                                0, -offset)
+                ));
+    }
+
+    /**
+     * Enables parallax background using a custom cover drawable at top and a custom bottom
+     * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+     *
+     * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}
+     *                      will not work if coverDrawable is not {@link FitWidthBitmapDrawable};
+     *                      in that case it's app's responsibility to set content into
+     *                      coverDrawable.
+     * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work
+     *                       if bottomDrawable is not {@link ColorDrawable}; in that case it's app's
+     *                       responsibility to set content of bottomDrawable.
+     * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.
+     *                                    Use null for no parallax movement effect.
+     *                                    Example to move bitmap within FitWidthBitmapDrawable:
+     *                                    new ParallaxTarget.PropertyValuesHolderTarget(
+     *                                        coverDrawable, PropertyValuesHolder.ofInt(
+     *                                            FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+     *                                            0, -120))
+     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+     */
+    public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
+                               @Nullable ParallaxTarget.PropertyValuesHolderTarget
+                                       coverDrawableParallaxTarget) {
+        if (mParallaxDrawable != null) {
+            return;
+        }
+        // if bitmap is set before enableParallax, use it as initial value.
+        if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {
+            ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);
+        }
+        // if solid color is set before enableParallax, use it as initial value.
+        if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {
+            ((ColorDrawable) bottomDrawable).setColor(mSolidColor);
+        }
+        if (mPlaybackGlue != null) {
+            throw new IllegalStateException("enableParallaxDrawable must be called before "
+                    + "enableVideoPlayback");
+        }
+        mParallaxDrawable = new DetailsParallaxDrawable(
+                FragmentUtil.getContext(mFragment),
+                mFragment.getParallax(),
+                coverDrawable,
+                bottomDrawable,
+                coverDrawableParallaxTarget);
+        mFragment.setBackgroundDrawable(mParallaxDrawable);
+    }
+
+    /**
+     * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
+     * creates a VideoFragment and VideoFragmentGlueHost to host the PlaybackGlue.
+     * This method must be called after calling details Fragment super.onCreate().
+     *
+     * @param playbackGlue
+     * @see #onCreateVideoFragment()
+     * @see #onCreateGlueHost().
+     */
+    public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
+        if (mPlaybackGlue == playbackGlue) {
+            return;
+        }
+        mPlaybackGlue = playbackGlue;
+        mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
+                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
+        if (mCanUseHost) {
+            mPlaybackGlue.setHost(onCreateGlueHost());
+        }
+    }
+
+    /**
+     * When fragment is started and no running transition. First set host if not yet set, second
+     * start playing if it was paused before.
+     */
+    void onStart() {
+        if (!mCanUseHost) {
+            mCanUseHost = true;
+            if (mPlaybackGlue != null) {
+                mPlaybackGlue.setHost(onCreateGlueHost());
+            }
+        }
+        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+            mPlaybackGlue.play();
+        }
+    }
+
+    void onStop() {
+        if (mPlaybackGlue != null) {
+            mPlaybackGlue.pause();
+        }
+    }
+
+    /**
+     * Disable parallax that would auto-start video playback
+     * @return true if video fragment is visible or false otherwise.
+     */
+    boolean disableVideoParallax() {
+        if (mVideoHelper != null) {
+            mVideoHelper.stopParallax();
+            return mVideoHelper.isVideoVisible();
+        }
+        return false;
+    }
+
+    /**
+     * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.
+     * By default it's a {@link FitWidthBitmapDrawable}.
+     *
+     * @return The cover drawable at top.
+     */
+    public final Drawable getCoverDrawable() {
+        if (mParallaxDrawable == null) {
+            return null;
+        }
+        return mParallaxDrawable.getCoverDrawable();
+    }
+
+    /**
+     * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.
+     * By default it's a {@link ColorDrawable}.
+     *
+     * @return The bottom drawable.
+     */
+    public final Drawable getBottomDrawable() {
+        if (mParallaxDrawable == null) {
+            return null;
+        }
+        return mParallaxDrawable.getBottomDrawable();
+    }
+
+    /**
+     * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoFragment} by
+     * default. App may override and return a different fragment and it also must override
+     * {@link #onCreateGlueHost()}.
+     *
+     * @return A new fragment used in {@link #onCreateGlueHost()}.
+     * @see #onCreateGlueHost()
+     * @see #setupVideoPlayback(PlaybackGlue)
+     */
+    public Fragment onCreateVideoFragment() {
+        return new VideoFragment();
+    }
+
+    /**
+     * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
+     * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment
+     * super.onCreate(). When override this method, app may call
+     * {@link #findOrCreateVideoFragment()} to get or create a fragment.
+     *
+     * @return A new PlaybackGlueHost to host PlaybackGlue.
+     * @see #onCreateVideoFragment()
+     * @see #findOrCreateVideoFragment()
+     * @see #setupVideoPlayback(PlaybackGlue)
+     */
+    public PlaybackGlueHost onCreateGlueHost() {
+        return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());
+    }
+
+    /**
+     * Adds or gets fragment for rendering video in DetailsFragment. A subclass that
+     * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
+     * a {@link PlaybackGlueHost}.
+     *
+     * @return Fragment the added or restored fragment responsible for rendering video.
+     * @see #onCreateGlueHost()
+     */
+    public final Fragment findOrCreateVideoFragment() {
+        return mFragment.findOrCreateVideoFragment();
+    }
+
+    /**
+     * Convenient method to set Bitmap in cover drawable. If app is not using default
+     * {@link FitWidthBitmapDrawable}, app should not use this method  It's safe to call
+     * setCoverBitmap() before calling {@link #enableParallax()}.
+     *
+     * @param bitmap bitmap to set as cover.
+     */
+    public final void setCoverBitmap(Bitmap bitmap) {
+        mCoverBitmap = bitmap;
+        Drawable drawable = getCoverDrawable();
+        if (drawable instanceof FitWidthBitmapDrawable) {
+            ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);
+        }
+    }
+
+    /**
+     * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.
+     *
+     * @return Bitmap for cover drawable.
+     */
+    public final Bitmap getCoverBitmap() {
+        return mCoverBitmap;
+    }
+
+    /**
+     * Returns color set by {@link #setSolidColor(int)}.
+     *
+     * @return Solid color used for bottom drawable.
+     */
+    public final @ColorInt int getSolidColor() {
+        return mSolidColor;
+    }
+
+    /**
+     * Convenient method to set color in bottom drawable. If app is not using default
+     * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()
+     * before calling {@link #enableParallax()}.
+     *
+     * @param color color for bottom drawable.
+     */
+    public final void setSolidColor(@ColorInt int color) {
+        mSolidColor = color;
+        Drawable bottomDrawable = getBottomDrawable();
+        if (bottomDrawable instanceof ColorDrawable) {
+            ((ColorDrawable) bottomDrawable).setColor(color);
+        }
+    }
+
+    /**
+     * Sets default parallax offset in pixels for bitmap moving vertically. This method must
+     * be called before {@link #enableParallax()}.
+     *
+     * @param offset Offset in pixels (e.g. 120).
+     * @see #enableParallax()
+     */
+    public final void setParallaxDrawableMaxOffset(int offset) {
+        if (mParallaxDrawable != null) {
+            throw new IllegalStateException("enableParallax already called");
+        }
+        mParallaxDrawableMaxOffset = offset;
+    }
+
+    /**
+     * Returns Default parallax offset in pixels for bitmap moving vertically.
+     * When 0, a default value would be used.
+     *
+     * @return Default parallax offset in pixels for bitmap moving vertically.
+     * @see #enableParallax()
+     */
+    public final int getParallaxDrawableMaxOffset() {
+        return mParallaxDrawableMaxOffset;
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentVideoHelper.java
deleted file mode 100644
index 415b850..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentVideoHelper.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.support.v17.leanback.app;
-
-import android.animation.Animator;
-import android.animation.ValueAnimator;
-import android.graphics.drawable.Drawable;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.widget.Parallax;
-import android.support.v17.leanback.widget.ParallaxRecyclerViewSource;
-import android.support.v17.leanback.widget.ParallaxTarget;
-
-/**
- * Helper class responsible for setting up video playback in {@link DetailsFragment}. This
- * takes {@link DetailsFragment} and {@link PlaybackGlue} as input and configures them. This
- * class is also responsible for implementing
- * {@link android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener} and
- * {@link android.support.v7.widget.RecyclerView.OnScrollListener} in {@link DetailsFragment}.
- * @hide
- */
-public class DetailsFragmentVideoHelper {
-    private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
-    private static final long CROSSFADE_DELAY = 1000;
-
-    /**
-     * Different states {@link DetailsFragment} can be in.
-     */
-    enum STATE {
-        INITIAL,
-        PLAY_VIDEO,
-        NO_VIDEO
-    }
-
-    private final DetailsParallaxManager mParallaxManager;
-    private STATE mCurrentState = STATE.INITIAL;
-
-    private ValueAnimator mBackgroundAnimator;
-    private Drawable mBackgroundDrawable;
-    private PlaybackGlue mPlaybackGlue;
-
-    /**
-     * Constructor.
-     */
-    public DetailsFragmentVideoHelper(
-            PlaybackGlue playbackGlue,
-            DetailsParallaxManager parallaxManager) {
-        this.mPlaybackGlue = playbackGlue;
-        this.mParallaxManager = parallaxManager;
-        setupParallax();
-    }
-
-    void setupParallax() {
-        Parallax parallax = mParallaxManager.getParallax();
-        ParallaxRecyclerViewSource.ChildPositionProperty frameTop = mParallaxManager.getFrameTop();
-        final float maxFrameTop = 1f;
-        final float minFrameTop = 0f;
-        parallax.addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))
-                .target(new ParallaxTarget() {
-
-                    float mFraction;
-                    @Override
-                    public void update(float fraction) {
-                        if (fraction == maxFrameTop) {
-                            updateState(STATE.NO_VIDEO);
-                        } else {
-                            updateState(STATE.PLAY_VIDEO);
-                        }
-                        mFraction = fraction;
-                    }
-
-                    @Override
-                    public float getFraction() {
-                        return mFraction;
-                    }
-                });
-    }
-
-    private void updateState(STATE state) {
-        if (state == mCurrentState) {
-            return;
-        }
-        mCurrentState = state;
-        switch (state) {
-            case PLAY_VIDEO:
-                if (mPlaybackGlue.isReadyForPlayback()) {
-                    internalStartPlayback();
-                } else {
-                    mPlaybackGlue.setPlayerCallback(new PlaybackControlStateCallback());
-                }
-                break;
-            case NO_VIDEO:
-                crossFadeBackgroundToVideo(false);
-                mPlaybackGlue.setPlayerCallback(null);
-                mPlaybackGlue.pause();
-                break;
-        }
-    }
-
-    private void internalStartPlayback() {
-        mPlaybackGlue.play();
-        mParallaxManager.getRecyclerView().postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                crossFadeBackgroundToVideo(true);
-            }
-        }, CROSSFADE_DELAY);
-    }
-
-    private void crossFadeBackgroundToVideo(final boolean crossFadeToVideo) {
-        if (mBackgroundAnimator != null) {
-            mBackgroundAnimator.cancel();
-        }
-
-        float startAlpha = crossFadeToVideo ? 1f : 0f;
-        float endAlpha = crossFadeToVideo ? 0f : 1f;
-
-        mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
-        mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
-        mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mBackgroundDrawable.setAlpha(
-                        (int) ((Float) (valueAnimator.getAnimatedValue()) * 255));
-            }
-        });
-
-        mBackgroundAnimator.addListener(new Animator.AnimatorListener() {
-            @Override
-            public void onAnimationStart(Animator animator) {
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mBackgroundAnimator = null;
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animator) {
-            }
-
-            @Override
-            public void onAnimationRepeat(Animator animator) {
-            }
-        });
-
-        mBackgroundAnimator.start();
-    }
-
-    /**
-     * Sets the drawable to be used as background image for {@link DetailsFragment}. If set,
-     * we will cross fade from the background drawable to the video.
-     */
-    public void setBackgroundDrawable(Drawable drawable) {
-        this.mBackgroundDrawable = drawable;
-    }
-
-    private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {
-
-        @Override
-        public void onReadyForPlayback() {
-            internalStartPlayback();
-        }
-    }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsParallaxManager.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsParallaxManager.java
deleted file mode 100644
index 12dd108..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsParallaxManager.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.Parallax;
-import android.support.v17.leanback.widget.ParallaxRecyclerViewSource;
-import android.support.v7.widget.RecyclerView;
-
-/**
- * Class in charge of managing the {@link Parallax} object for {@link DetailsFragment}. This
- * can be shared for creating both parallax effect and video animations when transitioning to/from
- * half/full screen.
- * @hide
- */
-public class DetailsParallaxManager {
-    final ParallaxRecyclerViewSource mParallaxSource;
-    final ParallaxRecyclerViewSource.ChildPositionProperty mFrameTop;
-    final ParallaxRecyclerViewSource.ChildPositionProperty mFrameBottom;
-    final Parallax mParallax;
-
-    public DetailsParallaxManager() {
-        mParallaxSource = new ParallaxRecyclerViewSource();
-
-        // track the top edge of details_frame of first item of adapter
-        mFrameTop = mParallaxSource
-                .addProperty("frameTop")
-                .adapterPosition(0)
-                .viewId(R.id.details_frame);
-
-        // track the bottom edge of details_frame of first item of adapter
-        mFrameBottom = mParallaxSource
-                .addProperty("frameBottom")
-                .adapterPosition(0)
-                .viewId(R.id.details_frame)
-                .fraction(1.0f);
-
-        mParallax = new Parallax();
-        mParallax.setSource(mParallaxSource);
-    }
-
-    /**
-     * Returns the {@link Parallax} instance.
-     */
-    public Parallax getParallax() {
-        return mParallax;
-    }
-
-    public RecyclerView getRecyclerView() {
-        return mParallaxSource.getRecyclerView();
-    }
-
-    /**
-     * Set the RecyclerView to register onScrollListener.
-     * @param recyclerView
-     */
-    public void setRecyclerView(RecyclerView recyclerView) {
-        mParallaxSource.setRecyclerView(recyclerView);
-    }
-
-    /**
-     * Returns the top of the details overview row. This is tracked for implementing the
-     * parallax effect.
-     */
-    public ParallaxRecyclerViewSource.ChildPositionProperty getFrameTop() {
-        return mFrameTop;
-    }
-
-    /**
-     * Returns the bottom of the details overview row. This is tracked for implementing the
-     * parallax effect.
-     */
-    public ParallaxRecyclerViewSource.ChildPositionProperty getFrameBottom() {
-        return mFrameBottom;
-    }
-}
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 a9bbf28..d2e0ef2 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -16,16 +16,21 @@
  */
 package android.support.v17.leanback.app;
 
+import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.CallSuper;
 import android.support.v17.leanback.R;
-import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
 import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.DetailsParallax;
 import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
 import android.support.v17.leanback.widget.ItemAlignmentFacet;
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
@@ -40,6 +45,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import java.lang.ref.WeakReference;
+
 /**
  * A fragment for creating Leanback details screens.
  *
@@ -78,6 +85,23 @@
     static final String TAG = "DetailsSupportFragment";
     static boolean DEBUG = false;
 
+    /**
+     * Flag for "possibly" having enter transition not finished yet.
+     * @see #mStartAndTransitionFlag
+     */
+    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;
+
     private class SetSelectionRunnable implements Runnable {
         int mPosition;
         boolean mSmooth = true;
@@ -94,14 +118,91 @@
         }
     }
 
+    /**
+     * 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) {
+            if (mWaitEnterTransitionTimeout != null) {
+                // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
+                // when transition finishes.
+                mWaitEnterTransitionTimeout.mRef.clear();
+            }
+        }
+
+        @Override
+        public void onTransitionCancel(Object transition) {
+            clearPendingEnterTransition();
+        }
+
+        @Override
+        public void onTransitionEnd(Object transition) {
+            clearPendingEnterTransition();
+        }
+    };
+
+    TransitionListener mReturnTransitionListener = new TransitionListener() {
+        @Override
+        public void onTransitionStart(Object transition) {
+            onReturnTransitionStart();
+        }
+    };
+
     BrowseFrameLayout mRootView;
+    View mBackgroundView;
+    Drawable mBackgroundDrawable;
     Fragment mVideoSupportFragment;
-    DetailsParallaxManager mDetailsParallaxManager;
+    DetailsParallax mDetailsParallax;
     RowsSupportFragment mRowsSupportFragment;
     ObjectAdapter mAdapter;
     int mContainerListAlignTop;
     BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener;
     BaseOnItemViewClickedListener mOnItemViewClickedListener;
+    DetailsSupportFragmentBackgroundController mDetailsBackgroundController;
+
+
+    /**
+     * 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;
 
@@ -180,6 +281,19 @@
         super.onCreate(savedInstanceState);
         mContainerListAlignTop =
             getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+
+        FragmentActivity activity = getActivity();
+        if (activity != null) {
+            Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
+            if (transition != null) {
+                mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
+                TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+            }
+            transition = TransitionHelper.getReturnTransition(activity.getWindow());
+            if (transition != null) {
+                TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
+            }
+        }
     }
 
     @Override
@@ -187,6 +301,10 @@
             Bundle savedInstanceState) {
         mRootView = (BrowseFrameLayout) inflater.inflate(
                 R.layout.lb_details_fragment, container, false);
+        mBackgroundView = mRootView.findViewById(R.id.details_background_view);
+        if (mBackgroundView != null) {
+            mBackgroundView.setBackground(mBackgroundDrawable);
+        }
         mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(
                 R.id.details_rows_dock);
         if (mRowsSupportFragment == null) {
@@ -213,13 +331,13 @@
             mRowsSupportFragment.setExternalAdapterListener(new ItemBridgeAdapter.AdapterListener() {
                 @Override
                 public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
-                    if (mDetailsParallaxManager != null && vh.getViewHolder()
+                    if (mDetailsParallax != null && vh.getViewHolder()
                             instanceof FullWidthDetailsOverviewRowPresenter.ViewHolder) {
                         FullWidthDetailsOverviewRowPresenter.ViewHolder rowVh =
                                 (FullWidthDetailsOverviewRowPresenter.ViewHolder)
                                         vh.getViewHolder();
                         rowVh.getOverviewView().setTag(R.id.lb_parallax_source,
-                                mDetailsParallaxManager.getParallax().getSource());
+                                mDetailsParallax);
                     }
                 }
             });
@@ -330,56 +448,26 @@
     }
 
     /**
-     * Creates an instance of {@link VideoSupportFragment}. Subclasses can override this method
-     * and provide their own instance of a {@link Fragment}. When you provide your own instance of
-     * video fragment, you MUST also provide a custom
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost}.
-     * @hide
-     */
-    public Fragment onCreateVideoSupportFragment() {
-        return new VideoSupportFragment();
-    }
-
-    /**
-     * Creates an instance of
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost}. The implementation
-     * of this host depends on the instance of video fragment {@link #onCreateVideoSupportFragment()}.
-     * @hide
-     */
-    public PlaybackGlueHost onCreateVideoSupportFragmentHost(Fragment fragment) {
-        return new VideoSupportFragmentGlueHost((VideoSupportFragment) fragment);
-    }
-
-    /**
-     * This method adds a fragment for rendering video to the layout. In case the
-     * fragment is being restored, it will return the video fragment in there.
+     * 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
+     * called after calling super.onCreate(). App usually does not call this method directly.
      *
      * @return Fragment the added or restored fragment responsible for rendering video.
-     * @hide
+     * @see DetailsSupportFragmentBackgroundController#onCreateVideoSupportFragment()
      */
-    public final Fragment findOrCreateVideoSupportFragment() {
-        Fragment fragment = getFragmentManager().findFragmentById(R.id.video_surface_container);
-        if (fragment == null) {
-            FragmentTransaction ft2 = getFragmentManager().beginTransaction();
+    final Fragment findOrCreateVideoSupportFragment() {
+        Fragment fragment = getChildFragmentManager()
+                .findFragmentById(R.id.video_surface_container);
+        if (fragment == null && mDetailsBackgroundController != null) {
+            FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
             ft2.add(android.support.v17.leanback.R.id.video_surface_container,
-                    fragment = onCreateVideoSupportFragment());
+                    fragment = mDetailsBackgroundController.onCreateVideoSupportFragment());
             ft2.commit();
         }
         mVideoSupportFragment = fragment;
         return mVideoSupportFragment;
     }
 
-    /**
-     * This method initializes a video fragment, create an instance of
-     * {@link android.support.v17.leanback.media.PlaybackGlueHost} using that fragment
-     * and return it.
-     * @hide
-     */
-    public final PlaybackGlueHost createPlaybackGlueHost() {
-        Fragment fragment = findOrCreateVideoSupportFragment();
-        return onCreateVideoSupportFragmentHost(fragment);
-    }
-
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
         if (( mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
@@ -394,6 +482,11 @@
         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);
+                }
+            }
             for (int i = 0; i < count; i++) {
                 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
                         gridView.getChildViewHolder(gridView.getChildAt(i));
@@ -406,6 +499,60 @@
         }
     }
 
+    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.
+     */
+    @CallSuper
+    void onSafeStart() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStart();
+        }
+    }
+
+    @CallSuper
+    void onReturnTransitionStart() {
+        if (mDetailsBackgroundController != null) {
+            // first disable parallax effect that auto-start PlaybackGlue.
+            boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
+            // if video is not visible we can safely remove VideoSupportFragment,
+            // otherwise let video playing during return transition.
+            if (!isVideoVisible && mVideoSupportFragment != null) {
+                FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+                ft2.remove(mVideoSupportFragment);
+                ft2.commit();
+                mVideoSupportFragment = null;
+            }
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStop();
+        }
+        super.onStop();
+    }
+
     /**
      * Called on every visible row to change view status when current selected row position
      * or selected sub position changed.  Subclass may override.   The default
@@ -464,19 +611,23 @@
     @Override
     public void onStart() {
         super.onStart();
+
+        mStartAndTransitionFlag |= PF_PENDING_START;
+        dispatchOnStartAndTransitionFinished();
+
         setupChildFragmentLayout();
         if (isEntranceTransitionEnabled()) {
             mRowsSupportFragment.setEntranceTransitionState(false);
         }
-        if (mDetailsParallaxManager != null) {
-            mDetailsParallaxManager.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
+        if (mDetailsParallax != null) {
+            mDetailsParallax.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
         }
         mRowsSupportFragment.getVerticalGridView().requestFocus();
     }
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(getContext(),
                 R.transition.lb_details_enter_transition);
     }
 
@@ -487,11 +638,14 @@
 
     @Override
     protected void onEntranceTransitionEnd() {
+        mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
+        dispatchOnStartAndTransitionFinished();
         mRowsSupportFragment.onTransitionEnd();
     }
 
     @Override
     protected void onEntranceTransitionPrepare() {
+        mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
         mRowsSupportFragment.onTransitionPrepare();
     }
 
@@ -501,44 +655,34 @@
     }
 
     /**
-     * Create a DetailsParallaxManager that will be used to configure parallax effect of background
-     * and start/stop Video playback. Subclass may override.
+     * Returns the {@link DetailsParallax} instance used by
+     * {@link DetailsSupportFragmentBackgroundController} to configure parallax effect of background and
+     * control embedded video playback. App usually does not use this method directly.
+     * App may use this method for other custom parallax tasks.
      *
-     * @return The new created DetailsParallaxManager.
-     * @see #getParallaxManager()
-     * @hide
+     * @return The DetailsParallax instance attached to the DetailsSupportFragment.
      */
-    public DetailsParallaxManager onCreateParallaxManager() {
-        return new DetailsParallaxManager();
-    }
-
-    /**
-     * Returns the {@link DetailsParallaxManager} instance used to configure parallax effect of
-     * background.
-     *
-     * @return The DetailsParallaxManager instance attached to the DetailsSupportFragment.
-     * @see #onCreateParallaxManager()
-     * @hide
-     */
-    public DetailsParallaxManager getParallaxManager() {
-        if (mDetailsParallaxManager == null) {
-            mDetailsParallaxManager = onCreateParallaxManager();
+    public DetailsParallax getParallax() {
+        if (mDetailsParallax == null) {
+            mDetailsParallax = new DetailsParallax();
             if (mRowsSupportFragment != null && mRowsSupportFragment.getView() != null) {
-                mDetailsParallaxManager.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
+                mDetailsParallax.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
             }
         }
-        return mDetailsParallaxManager;
+        return mDetailsParallax;
     }
 
     /**
-     * Returns background View that above VideoSupportFragment. App can set a background drawable to this
-     * view to hide the VideoSupportFragment before it is ready to play.
+     * Set background drawable shown below foreground rows UI and above
+     * {@link #findOrCreateVideoSupportFragment()}.
      *
-     * @see #findOrCreateVideoSupportFragment()
-     * @hide
+     * @see DetailsSupportFragmentBackgroundController
      */
-    public View getBackgroundView() {
-        return mRootView == null ? null : mRootView.findViewById(R.id.details_background_view);
+    void setBackgroundDrawable(Drawable drawable) {
+        if (mBackgroundView != null) {
+            mBackgroundView.setBackground(drawable);
+        }
+        mBackgroundDrawable = drawable;
     }
 
     /**
@@ -552,6 +696,27 @@
      * </ul>
      */
     void setupDpadNavigation() {
+        mRootView.setOnChildFocusListener(new BrowseFrameLayout.OnChildFocusListener() {
+
+            @Override
+            public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+                return false;
+            }
+
+            @Override
+            public void onRequestChildFocus(View child, View focused) {
+                if (child != mRootView.getFocusedChild()) {
+                    if (child.getId() == R.id.details_fragment_root) {
+                        showTitle(true);
+                    } else if (child.getId() == R.id.video_surface_container) {
+                        slideOutGridView();
+                        showTitle(false);
+                    } else {
+                        showTitle(true);
+                    }
+                }
+            }
+        });
         mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
             @Override
             public View onFocusSearch(View focused, int direction) {
@@ -559,10 +724,8 @@
                         && mRowsSupportFragment.getVerticalGridView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
                         if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null) {
-                            slideOutGridView();
-                            showTitle(false);
                             return mVideoSupportFragment.getView();
-                        } else if (getTitleView() != null) {
+                        } else if (getTitleView() != null && getTitleView().hasFocusable()) {
                             return getTitleView();
                         }
                     }
@@ -570,8 +733,6 @@
                         && mVideoSupportFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_DOWN) {
                         if (mRowsSupportFragment.getVerticalGridView() != null) {
-                            showTitle(true);
-                            slideInGridView();
                             return mRowsSupportFragment.getVerticalGridView();
                         }
                     }
@@ -586,7 +747,7 @@
             }
         });
 
-        // If we press BACK or DOWN on remote while in full screen video mode, we should
+        // If we press BACK on remote while in full screen video mode, we should
         // transition back to half screen video playback mode.
         mRootView.setOnDispatchKeyListener(new View.OnKeyListener() {
             @Override
@@ -596,9 +757,7 @@
                 // focusability of the video surface view.
                 if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
                         && mVideoSupportFragment.getView().hasFocus()) {
-                    if (keyCode == KeyEvent.KEYCODE_BACK) {
-                        showTitle(true);
-                        slideInGridView();
+                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
                         getVerticalGridView().requestFocus();
                         return true;
                     }
@@ -613,13 +772,9 @@
      * Slides vertical grid view (displaying media item details) out of the screen from below.
      */
     void slideOutGridView() {
-        getVerticalGridView().animateOut();
+        if (getVerticalGridView() != null) {
+            getVerticalGridView().animateOut();
+        }
     }
 
-    /**
-     * Slides in vertical grid view (displaying media item details) from below.
-     */
-    void slideInGridView() {
-        getVerticalGridView().animateIn();
-    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
new file mode 100644
index 0000000..071a04a
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -0,0 +1,406 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoDetailsFragmentBackgroundController.java.  DO NOT MODIFY. */
+
+/*
+ * 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.app;
+
+import android.animation.PropertyValuesHolder;
+import android.support.v4.app.Fragment;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+
+/**
+ * Controller for DetailsSupportFragment parallax background and embedded video play.
+ * <p>
+ * The parallax background drawable is made of two parts: cover drawable (by default
+ * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default
+ * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size
+ * of cover drawable and bottom drawable will be updated and the cover drawable will by default
+ * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.
+ * </p>
+ * <pre>
+ *        ***************************
+ *        *      Cover Drawable     *
+ *        * (FitWidthBitmapDrawable)*
+ *        *                         *
+ *        ***************************
+ *        *    DetailsOverviewRow   *
+ *        *                         *
+ *        ***************************
+ *        *     Bottom Drawable     *
+ *        *      (ColorDrawable)    *
+ *        *         Related         *
+ *        *         Content         *
+ *        ***************************
+ * </pre>
+ * Both parallax background drawable and embedded video play are optional. App must call
+ * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.
+ * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and
+ * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable
+ * will be faded out.
+ * Example:
+ * <pre>
+ * DetailsSupportFragmentBackgroundController mController = new DetailsSupportFragmentBackgroundController(this);
+ *
+ * public void onCreate(Bundle savedInstance) {
+ *     super.onCreate(savedInstance);
+ *     MediaPlayerGlue player = new MediaPlayerGlue(..);
+ *     player.setUrl(...);
+ *     mController.enableParallax();
+ *     mController.setupVideoPlayback(player);
+ * }
+ *
+ * static class MyLoadBitmapTask extends ... {
+ *     WeakReference<MyFragment> mFragmentRef;
+ *     MyLoadBitmapTask(MyFragment fragment) {
+ *         mFragmentRef = new WeakReference(fragment);
+ *     }
+ *     protected void onPostExecute(Bitmap bitmap) {
+ *         MyFragment fragment = mFragmentRef.get();
+ *         if (fragment != null) {
+ *             fragment.mController.setCoverBitmap(bitmap);
+ *         }
+ *     }
+ * }
+ *
+ * public void onStart() {
+ *     new MyLoadBitmapTask(this).execute(url);
+ * }
+ *
+ * public void onStop() {
+ *     mController.setCoverBitmap(null);
+ * }
+ * </pre>
+ * <p>
+ * To customize cover drawable and/or bottom drawable, app should call
+ * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.
+ * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.
+ * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.
+ * </p>
+ * <p>
+ * To customize playback fragment, app should override {@link #onCreateVideoSupportFragment()} and
+ * {@link #onCreateGlueHost()}.
+ * </p>
+ *
+ */
+public class DetailsSupportFragmentBackgroundController {
+
+    private final DetailsSupportFragment mFragment;
+    private DetailsParallaxDrawable mParallaxDrawable;
+    private int mParallaxDrawableMaxOffset;
+    private PlaybackGlue mPlaybackGlue;
+    private DetailsBackgroundVideoHelper mVideoHelper;
+    private Bitmap mCoverBitmap;
+    private int mSolidColor;
+    private boolean mCanUseHost = false;
+
+    /**
+     * Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that
+     * each DetailsSupportFragment can only associate with one DetailsSupportFragmentBackgroundController.
+     *
+     * @param fragment The DetailsSupportFragment to control background and embedded video playing.
+     * @throws IllegalStateException If fragment was already associated with another controller.
+     */
+    public DetailsSupportFragmentBackgroundController(DetailsSupportFragment fragment) {
+        if (fragment.mDetailsBackgroundController != null) {
+            throw new IllegalStateException("Each DetailsSupportFragment is allowed to initialize "
+                    + "DetailsSupportFragmentBackgroundController once");
+        }
+        fragment.mDetailsBackgroundController = this;
+        mFragment = fragment;
+    }
+
+    /**
+     * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable
+     * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied
+     * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and
+     * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.
+     * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+     *
+     * @see #setCoverBitmap(Bitmap)
+     * @see #setSolidColor(int)
+     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+     */
+    public void enableParallax() {
+        int offset = mParallaxDrawableMaxOffset;
+        if (offset == 0) {
+            offset = mFragment.getContext().getResources()
+                    .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);
+        }
+        Drawable coverDrawable = new FitWidthBitmapDrawable();
+        ColorDrawable colorDrawable = new ColorDrawable();
+        enableParallax(coverDrawable, colorDrawable,
+                new ParallaxTarget.PropertyValuesHolderTarget(
+                        coverDrawable,
+                        PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+                                0, -offset)
+                ));
+    }
+
+    /**
+     * Enables parallax background using a custom cover drawable at top and a custom bottom
+     * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+     *
+     * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}
+     *                      will not work if coverDrawable is not {@link FitWidthBitmapDrawable};
+     *                      in that case it's app's responsibility to set content into
+     *                      coverDrawable.
+     * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work
+     *                       if bottomDrawable is not {@link ColorDrawable}; in that case it's app's
+     *                       responsibility to set content of bottomDrawable.
+     * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.
+     *                                    Use null for no parallax movement effect.
+     *                                    Example to move bitmap within FitWidthBitmapDrawable:
+     *                                    new ParallaxTarget.PropertyValuesHolderTarget(
+     *                                        coverDrawable, PropertyValuesHolder.ofInt(
+     *                                            FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+     *                                            0, -120))
+     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+     */
+    public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
+                               @Nullable ParallaxTarget.PropertyValuesHolderTarget
+                                       coverDrawableParallaxTarget) {
+        if (mParallaxDrawable != null) {
+            return;
+        }
+        // if bitmap is set before enableParallax, use it as initial value.
+        if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {
+            ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);
+        }
+        // if solid color is set before enableParallax, use it as initial value.
+        if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {
+            ((ColorDrawable) bottomDrawable).setColor(mSolidColor);
+        }
+        if (mPlaybackGlue != null) {
+            throw new IllegalStateException("enableParallaxDrawable must be called before "
+                    + "enableVideoPlayback");
+        }
+        mParallaxDrawable = new DetailsParallaxDrawable(
+                mFragment.getContext(),
+                mFragment.getParallax(),
+                coverDrawable,
+                bottomDrawable,
+                coverDrawableParallaxTarget);
+        mFragment.setBackgroundDrawable(mParallaxDrawable);
+    }
+
+    /**
+     * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
+     * creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue.
+     * This method must be called after calling details Fragment super.onCreate().
+     *
+     * @param playbackGlue
+     * @see #onCreateVideoSupportFragment()
+     * @see #onCreateGlueHost().
+     */
+    public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
+        if (mPlaybackGlue == playbackGlue) {
+            return;
+        }
+        mPlaybackGlue = playbackGlue;
+        mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
+                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
+        if (mCanUseHost) {
+            mPlaybackGlue.setHost(onCreateGlueHost());
+        }
+    }
+
+    /**
+     * When fragment is started and no running transition. First set host if not yet set, second
+     * start playing if it was paused before.
+     */
+    void onStart() {
+        if (!mCanUseHost) {
+            mCanUseHost = true;
+            if (mPlaybackGlue != null) {
+                mPlaybackGlue.setHost(onCreateGlueHost());
+            }
+        }
+        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+            mPlaybackGlue.play();
+        }
+    }
+
+    void onStop() {
+        if (mPlaybackGlue != null) {
+            mPlaybackGlue.pause();
+        }
+    }
+
+    /**
+     * Disable parallax that would auto-start video playback
+     * @return true if video fragment is visible or false otherwise.
+     */
+    boolean disableVideoParallax() {
+        if (mVideoHelper != null) {
+            mVideoHelper.stopParallax();
+            return mVideoHelper.isVideoVisible();
+        }
+        return false;
+    }
+
+    /**
+     * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.
+     * By default it's a {@link FitWidthBitmapDrawable}.
+     *
+     * @return The cover drawable at top.
+     */
+    public final Drawable getCoverDrawable() {
+        if (mParallaxDrawable == null) {
+            return null;
+        }
+        return mParallaxDrawable.getCoverDrawable();
+    }
+
+    /**
+     * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.
+     * By default it's a {@link ColorDrawable}.
+     *
+     * @return The bottom drawable.
+     */
+    public final Drawable getBottomDrawable() {
+        if (mParallaxDrawable == null) {
+            return null;
+        }
+        return mParallaxDrawable.getBottomDrawable();
+    }
+
+    /**
+     * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoSupportFragment} by
+     * default. App may override and return a different fragment and it also must override
+     * {@link #onCreateGlueHost()}.
+     *
+     * @return A new fragment used in {@link #onCreateGlueHost()}.
+     * @see #onCreateGlueHost()
+     * @see #setupVideoPlayback(PlaybackGlue)
+     */
+    public Fragment onCreateVideoSupportFragment() {
+        return new VideoSupportFragment();
+    }
+
+    /**
+     * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
+     * {@link #onCreateVideoSupportFragment()}. This method must be called after calling Fragment
+     * super.onCreate(). When override this method, app may call
+     * {@link #findOrCreateVideoSupportFragment()} to get or create a fragment.
+     *
+     * @return A new PlaybackGlueHost to host PlaybackGlue.
+     * @see #onCreateVideoSupportFragment()
+     * @see #findOrCreateVideoSupportFragment()
+     * @see #setupVideoPlayback(PlaybackGlue)
+     */
+    public PlaybackGlueHost onCreateGlueHost() {
+        return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment());
+    }
+
+    /**
+     * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that
+     * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
+     * a {@link PlaybackGlueHost}.
+     *
+     * @return Fragment the added or restored fragment responsible for rendering video.
+     * @see #onCreateGlueHost()
+     */
+    public final Fragment findOrCreateVideoSupportFragment() {
+        return mFragment.findOrCreateVideoSupportFragment();
+    }
+
+    /**
+     * Convenient method to set Bitmap in cover drawable. If app is not using default
+     * {@link FitWidthBitmapDrawable}, app should not use this method  It's safe to call
+     * setCoverBitmap() before calling {@link #enableParallax()}.
+     *
+     * @param bitmap bitmap to set as cover.
+     */
+    public final void setCoverBitmap(Bitmap bitmap) {
+        mCoverBitmap = bitmap;
+        Drawable drawable = getCoverDrawable();
+        if (drawable instanceof FitWidthBitmapDrawable) {
+            ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);
+        }
+    }
+
+    /**
+     * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.
+     *
+     * @return Bitmap for cover drawable.
+     */
+    public final Bitmap getCoverBitmap() {
+        return mCoverBitmap;
+    }
+
+    /**
+     * Returns color set by {@link #setSolidColor(int)}.
+     *
+     * @return Solid color used for bottom drawable.
+     */
+    public final @ColorInt int getSolidColor() {
+        return mSolidColor;
+    }
+
+    /**
+     * Convenient method to set color in bottom drawable. If app is not using default
+     * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()
+     * before calling {@link #enableParallax()}.
+     *
+     * @param color color for bottom drawable.
+     */
+    public final void setSolidColor(@ColorInt int color) {
+        mSolidColor = color;
+        Drawable bottomDrawable = getBottomDrawable();
+        if (bottomDrawable instanceof ColorDrawable) {
+            ((ColorDrawable) bottomDrawable).setColor(color);
+        }
+    }
+
+    /**
+     * Sets default parallax offset in pixels for bitmap moving vertically. This method must
+     * be called before {@link #enableParallax()}.
+     *
+     * @param offset Offset in pixels (e.g. 120).
+     * @see #enableParallax()
+     */
+    public final void setParallaxDrawableMaxOffset(int offset) {
+        if (mParallaxDrawable != null) {
+            throw new IllegalStateException("enableParallax already called");
+        }
+        mParallaxDrawableMaxOffset = offset;
+    }
+
+    /**
+     * Returns Default parallax offset in pixels for bitmap moving vertically.
+     * When 0, a default value would be used.
+     *
+     * @return Default parallax offset in pixels for bitmap moving vertically.
+     * @see #enableParallax()
+     */
+    public final int getParallaxDrawableMaxOffset() {
+        return mParallaxDrawableMaxOffset;
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java b/v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
new file mode 100644
index 0000000..23c6039
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
@@ -0,0 +1,37 @@
+/*
+ * 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.app;
+
+import android.annotation.TargetApi;
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Build;
+
+class FragmentUtil {
+
+    @TargetApi(23)
+    private static Context getContextNew(Fragment fragment) {
+        return fragment.getContext();
+    }
+
+    public static Context getContext(Fragment fragment) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            return getContextNew(fragment);
+        } else {
+            return fragment.getActivity();
+        }
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
index da44fde..bab48bd 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -1129,7 +1129,7 @@
         } else {
             // when there are two actions panel, we need adjust the weight of action to
             // guidedActionContentWidthWeightTwoPanels.
-            Context ctx = mThemeWrapper != null ? mThemeWrapper : getActivity();
+            Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(this);
             TypedValue typedValue = new TypedValue();
             if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
                     typedValue, true)) {
@@ -1327,18 +1327,18 @@
     private void resolveTheme() {
         // Look up the guidedStepTheme in the currently specified theme.  If it exists,
         // replace the theme with its value.
-        Activity activity = getActivity();
+        Context context = FragmentUtil.getContext(this);
         int theme = onProvideTheme();
-        if (theme == -1 && !isGuidedStepTheme(activity)) {
+        if (theme == -1 && !isGuidedStepTheme(context)) {
             // Look up the guidedStepTheme in the activity's currently specified theme.  If it
             // exists, replace the theme with its value.
             int resId = R.attr.guidedStepTheme;
             TypedValue typedValue = new TypedValue();
-            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
+            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
             if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
             if (found) {
                 ContextThemeWrapper themeWrapper =
-                        new ContextThemeWrapper(activity, typedValue.resourceId);
+                        new ContextThemeWrapper(context, typedValue.resourceId);
                 if (isGuidedStepTheme(themeWrapper)) {
                     mThemeWrapper = themeWrapper;
                 } else {
@@ -1350,7 +1350,7 @@
                 Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
             }
         } else if (theme != -1) {
-            mThemeWrapper = new ContextThemeWrapper(activity, theme);
+            mThemeWrapper = new ContextThemeWrapper(context, theme);
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
index 32de1fd..f68366c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
@@ -1132,7 +1132,7 @@
         } else {
             // when there are two actions panel, we need adjust the weight of action to
             // guidedActionContentWidthWeightTwoPanels.
-            Context ctx = mThemeWrapper != null ? mThemeWrapper : getActivity();
+            Context ctx = mThemeWrapper != null ? mThemeWrapper : getContext();
             TypedValue typedValue = new TypedValue();
             if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
                     typedValue, true)) {
@@ -1330,18 +1330,18 @@
     private void resolveTheme() {
         // Look up the guidedStepTheme in the currently specified theme.  If it exists,
         // replace the theme with its value.
-        FragmentActivity activity = getActivity();
+        Context context = getContext();
         int theme = onProvideTheme();
-        if (theme == -1 && !isGuidedStepTheme(activity)) {
+        if (theme == -1 && !isGuidedStepTheme(context)) {
             // Look up the guidedStepTheme in the activity's currently specified theme.  If it
             // exists, replace the theme with its value.
             int resId = R.attr.guidedStepTheme;
             TypedValue typedValue = new TypedValue();
-            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
+            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
             if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
             if (found) {
                 ContextThemeWrapper themeWrapper =
-                        new ContextThemeWrapper(activity, typedValue.resourceId);
+                        new ContextThemeWrapper(context, typedValue.resourceId);
                 if (isGuidedStepTheme(themeWrapper)) {
                     mThemeWrapper = themeWrapper;
                 } else {
@@ -1353,7 +1353,7 @@
                 Log.e(TAG, "GuidedStepSupportFragment does not have an appropriate theme set.");
             }
         } else if (theme != -1) {
-            mThemeWrapper = new ContextThemeWrapper(activity, theme);
+            mThemeWrapper = new ContextThemeWrapper(context, theme);
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
index 18f96fe..417a2b6 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
@@ -34,12 +34,19 @@
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
 /**
- * An internal fragment containing a list of row headers.
+ * An fragment containing a list of row headers. Implementation must support three types of rows:
+ * <ul>
+ *     <li>{@link DividerRow} rendered by {@link DividerPresenter}.</li>
+ *     <li>{@link Row} rendered by {@link RowHeaderPresenter}.</li>
+ *     <li>{@link SectionRow} rendered by {@link RowHeaderPresenter}.</li>
+ * </ul>
+ * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize
+ * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}.
  */
 public class HeadersFragment extends BaseRowFragment {
 
@@ -158,9 +165,7 @@
         if (listView == null) {
             return;
         }
-        if (getBridgeAdapter() != null) {
-            FocusHighlightHelper.setupHeaderItemFocusHighlight(listView);
-        }
+        FocusHighlightHelper.setupHeaderItemFocusHighlight(listView);
         if (mBackgroundColorSet) {
             listView.setBackgroundColor(mBackgroundColor);
             updateFadingEdgeToBrandColor(mBackgroundColor);
@@ -229,13 +234,8 @@
     void updateAdapter() {
         super.updateAdapter();
         ItemBridgeAdapter adapter = getBridgeAdapter();
-        if (adapter != null) {
-            adapter.setAdapterListener(mAdapterListener);
-            adapter.setWrapper(mWrapper);
-        }
-        if (adapter != null && getVerticalGridView() != null) {
-            FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView());
-        }
+        adapter.setAdapterListener(mAdapterListener);
+        adapter.setWrapper(mWrapper);
     }
 
     void setBackgroundColor(int color) {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
index 8989edf..8112a8a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
@@ -37,12 +37,19 @@
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
 /**
- * An internal fragment containing a list of row headers.
+ * An fragment containing a list of row headers. Implementation must support three types of rows:
+ * <ul>
+ *     <li>{@link DividerRow} rendered by {@link DividerPresenter}.</li>
+ *     <li>{@link Row} rendered by {@link RowHeaderPresenter}.</li>
+ *     <li>{@link SectionRow} rendered by {@link RowHeaderPresenter}.</li>
+ * </ul>
+ * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize
+ * Presenters. App may override {@link BrowseSupportFragment#onCreateHeadersSupportFragment()}.
  */
 public class HeadersSupportFragment extends BaseRowSupportFragment {
 
@@ -161,9 +168,7 @@
         if (listView == null) {
             return;
         }
-        if (getBridgeAdapter() != null) {
-            FocusHighlightHelper.setupHeaderItemFocusHighlight(listView);
-        }
+        FocusHighlightHelper.setupHeaderItemFocusHighlight(listView);
         if (mBackgroundColorSet) {
             listView.setBackgroundColor(mBackgroundColor);
             updateFadingEdgeToBrandColor(mBackgroundColor);
@@ -232,13 +237,8 @@
     void updateAdapter() {
         super.updateAdapter();
         ItemBridgeAdapter adapter = getBridgeAdapter();
-        if (adapter != null) {
-            adapter.setAdapterListener(mAdapterListener);
-            adapter.setWrapper(mWrapper);
-        }
-        if (adapter != null && getVerticalGridView() != null) {
-            FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView());
-        }
+        adapter.setAdapterListener(mAdapterListener);
+        adapter.setWrapper(mWrapper);
     }
 
     void setBackgroundColor(int color) {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
index 1f2788f..f9af12f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
@@ -41,6 +41,7 @@
     }
 
     void initialize() {
+        mLastVisibleRowIndex = -1;
         int i = mAdapter.size() - 1;
         while (i >= 0) {
             Row item = (Row) mAdapter.get(i);
@@ -123,7 +124,7 @@
             int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
             if (totalItems > 0) {
                 onEventFired(ON_ITEM_RANGE_REMOVED,
-                        Math.min(lastVisibleRowIndex + 1, positionStart),
+                        Math.min(mLastVisibleRowIndex + 1, positionStart),
                         totalItems);
             }
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java b/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
index 0459ab6..1baffb4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
@@ -22,8 +22,8 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.app.Activity;
 import android.app.Fragment;
+import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.v17.leanback.R;
@@ -267,8 +267,9 @@
         mLogoView = (ImageView) view.findViewById(R.id.logo);
         mTitleView = (TextView) view.findViewById(R.id.title);
         mDescriptionView = (TextView) view.findViewById(R.id.description);
+        final Context context = FragmentUtil.getContext(this);
         if (sSlideDistance == 0) {
-            sSlideDistance = (int) (SLIDE_DISTANCE * getActivity().getResources()
+            sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
                     .getDisplayMetrics().scaledDensity);
         }
         if (savedInstanceState == null) {
@@ -312,20 +313,20 @@
     }
 
     private void resolveTheme() {
-        Activity activity = getActivity();
+        final Context context = FragmentUtil.getContext(this);
         int theme = onProvideTheme();
         if (theme == -1) {
             // Look up the onboardingTheme in the activity's currently specified theme. If it
             // exists, wrap the theme with its value.
             int resId = R.attr.onboardingTheme;
             TypedValue typedValue = new TypedValue();
-            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
+            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
             if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
             if (found) {
-                mThemeWrapper = new ContextThemeWrapper(activity, typedValue.resourceId);
+                mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
             }
         } else {
-            mThemeWrapper = new ContextThemeWrapper(activity, theme);
+            mThemeWrapper = new ContextThemeWrapper(context, theme);
         }
     }
 
@@ -366,13 +367,14 @@
     }
 
     boolean startLogoAnimation() {
+        final Context context = FragmentUtil.getContext(this);
         Animator animator = null;
         if (mLogoResourceId != 0) {
             mLogoView.setVisibility(View.VISIBLE);
             mLogoView.setImageResource(mLogoResourceId);
-            Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator inAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_logo_enter);
-            Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator outAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_logo_exit);
             outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
             AnimatorSet logoAnimator = new AnimatorSet();
@@ -386,7 +388,7 @@
             animator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    if (getActivity() != null) {
+                    if (context != null) {
                         startEnterAnimation();
                     }
                 }
@@ -411,7 +413,8 @@
     private void initializeViews(View container) {
         mLogoView.setVisibility(View.GONE);
         // Create custom views.
-        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(getActivity()));
+        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
+                FragmentUtil.getContext(this)));
         ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
                 R.id.background_container);
         View background = onCreateBackgroundView(inflater, backgroundContainer);
@@ -453,22 +456,23 @@
         mEnterTransitionFinished = true;
         initializeViews(getView());
         List<Animator> animators = new ArrayList<>();
-        Animator animator = AnimatorInflater.loadAnimator(getActivity(),
+        final Context context = FragmentUtil.getContext(this);
+        Animator animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_page_indicator_enter);
         animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
         animators.add(animator);
         // Header title
-        View view = getActivity().findViewById(R.id.title);
+        View view = getView().findViewById(R.id.title);
         view.setAlpha(0);
-        animator = AnimatorInflater.loadAnimator(getActivity(),
+        animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_title_enter);
         animator.setStartDelay(START_DELAY_TITLE_MS);
         animator.setTarget(view);
         animators.add(animator);
         // Header description
-        view = getActivity().findViewById(R.id.description);
+        view = getView().findViewById(R.id.description);
         view.setAlpha(0);
-        animator = AnimatorInflater.loadAnimator(getActivity(),
+        animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_description_enter);
         animator.setStartDelay(START_DELAY_DESCRIPTION_MS);
         animator.setTarget(view);
@@ -610,10 +614,11 @@
             }
         });
 
+        final Context context = FragmentUtil.getContext(this);
         // Animator for switching between page indicator and button.
         if (getCurrentPageIndex() == getPageCount() - 1) {
             mStartButton.setVisibility(View.VISIBLE);
-            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_page_indicator_fade_out);
             navigatorFadeOutAnimator.setTarget(mPageIndicator);
             navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
@@ -623,17 +628,17 @@
                 }
             });
             animators.add(navigatorFadeOutAnimator);
-            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_start_button_fade_in);
             buttonFadeInAnimator.setTarget(mStartButton);
             animators.add(buttonFadeInAnimator);
         } else if (previousPage == getPageCount() - 1) {
             mPageIndicator.setVisibility(View.VISIBLE);
-            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_page_indicator_fade_in);
             navigatorFadeInAnimator.setTarget(mPageIndicator);
             animators.add(navigatorFadeInAnimator);
-            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_start_button_fade_out);
             buttonFadeOutAnimator.setTarget(mStartButton);
             buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
index 32163b0..8523a27 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
@@ -25,8 +25,8 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.Fragment;
+import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.v17.leanback.R;
@@ -270,8 +270,9 @@
         mLogoView = (ImageView) view.findViewById(R.id.logo);
         mTitleView = (TextView) view.findViewById(R.id.title);
         mDescriptionView = (TextView) view.findViewById(R.id.description);
+        final Context context = getContext();
         if (sSlideDistance == 0) {
-            sSlideDistance = (int) (SLIDE_DISTANCE * getActivity().getResources()
+            sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
                     .getDisplayMetrics().scaledDensity);
         }
         if (savedInstanceState == null) {
@@ -315,20 +316,20 @@
     }
 
     private void resolveTheme() {
-        FragmentActivity activity = getActivity();
+        final Context context = getContext();
         int theme = onProvideTheme();
         if (theme == -1) {
             // Look up the onboardingTheme in the activity's currently specified theme. If it
             // exists, wrap the theme with its value.
             int resId = R.attr.onboardingTheme;
             TypedValue typedValue = new TypedValue();
-            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
+            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
             if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
             if (found) {
-                mThemeWrapper = new ContextThemeWrapper(activity, typedValue.resourceId);
+                mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
             }
         } else {
-            mThemeWrapper = new ContextThemeWrapper(activity, theme);
+            mThemeWrapper = new ContextThemeWrapper(context, theme);
         }
     }
 
@@ -369,13 +370,14 @@
     }
 
     boolean startLogoAnimation() {
+        final Context context = getContext();
         Animator animator = null;
         if (mLogoResourceId != 0) {
             mLogoView.setVisibility(View.VISIBLE);
             mLogoView.setImageResource(mLogoResourceId);
-            Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator inAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_logo_enter);
-            Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator outAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_logo_exit);
             outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
             AnimatorSet logoAnimator = new AnimatorSet();
@@ -389,7 +391,7 @@
             animator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    if (getActivity() != null) {
+                    if (context != null) {
                         startEnterAnimation();
                     }
                 }
@@ -414,7 +416,8 @@
     private void initializeViews(View container) {
         mLogoView.setVisibility(View.GONE);
         // Create custom views.
-        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(getActivity()));
+        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
+                getContext()));
         ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
                 R.id.background_container);
         View background = onCreateBackgroundView(inflater, backgroundContainer);
@@ -456,22 +459,23 @@
         mEnterTransitionFinished = true;
         initializeViews(getView());
         List<Animator> animators = new ArrayList<>();
-        Animator animator = AnimatorInflater.loadAnimator(getActivity(),
+        final Context context = getContext();
+        Animator animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_page_indicator_enter);
         animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
         animators.add(animator);
         // Header title
-        View view = getActivity().findViewById(R.id.title);
+        View view = getView().findViewById(R.id.title);
         view.setAlpha(0);
-        animator = AnimatorInflater.loadAnimator(getActivity(),
+        animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_title_enter);
         animator.setStartDelay(START_DELAY_TITLE_MS);
         animator.setTarget(view);
         animators.add(animator);
         // Header description
-        view = getActivity().findViewById(R.id.description);
+        view = getView().findViewById(R.id.description);
         view.setAlpha(0);
-        animator = AnimatorInflater.loadAnimator(getActivity(),
+        animator = AnimatorInflater.loadAnimator(context,
                 R.animator.lb_onboarding_description_enter);
         animator.setStartDelay(START_DELAY_DESCRIPTION_MS);
         animator.setTarget(view);
@@ -613,10 +617,11 @@
             }
         });
 
+        final Context context = getContext();
         // Animator for switching between page indicator and button.
         if (getCurrentPageIndex() == getPageCount() - 1) {
             mStartButton.setVisibility(View.VISIBLE);
-            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_page_indicator_fade_out);
             navigatorFadeOutAnimator.setTarget(mPageIndicator);
             navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
@@ -626,17 +631,17 @@
                 }
             });
             animators.add(navigatorFadeOutAnimator);
-            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_start_button_fade_in);
             buttonFadeInAnimator.setTarget(mStartButton);
             animators.add(buttonFadeInAnimator);
         } else if (previousPage == getPageCount() - 1) {
             mPageIndicator.setVisibility(View.VISIBLE);
-            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_page_indicator_fade_in);
             navigatorFadeInAnimator.setTarget(mPageIndicator);
             animators.add(navigatorFadeInAnimator);
-            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
+            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                     R.animator.lb_onboarding_start_button_fade_out);
             buttonFadeOutAnimator.setTarget(mStartButton);
             buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index 02a0257..8d6367a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -452,11 +452,12 @@
             }
         };
 
-        mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
         mBgFadeInAnimator.addUpdateListener(listener);
         mBgFadeInAnimator.addListener(mFadeListener);
 
-        mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out);
+        mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
         mBgFadeOutAnimator.addUpdateListener(listener);
         mBgFadeOutAnimator.addListener(mFadeListener);
     }
@@ -498,14 +499,14 @@
             }
         };
 
-        mControlRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
         mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mControlRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mControlRowFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
         mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
@@ -543,14 +544,13 @@
             }
         };
 
-        mOtherRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mOtherRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
         mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
@@ -808,7 +808,6 @@
     /**
      * This listener is called every time there is a selection in {@link RowsFragment}. This can
      * be used by users to take additional actions such as animations.
-     * @hide
      */
     public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
         mExternalItemSelectedListener = listener;
@@ -836,6 +835,14 @@
         super.onDestroyView();
     }
 
+    @Override
+    public void onDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+        super.onDestroy();
+    }
+
     /**
      * Sets the playback row for the playback controls.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
index 33e35eb..c601b2e 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
@@ -433,11 +433,12 @@
             }
         };
 
-        mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
         mBgFadeInAnimator.addUpdateListener(listener);
         mBgFadeInAnimator.addListener(mFadeListener);
 
-        mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out);
+        mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
         mBgFadeOutAnimator.addUpdateListener(listener);
         mBgFadeOutAnimator.addListener(mFadeListener);
     }
@@ -479,14 +480,14 @@
             }
         };
 
-        mControlRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
         mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mControlRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mControlRowFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
         mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
@@ -524,14 +525,13 @@
             }
         };
 
-        mOtherRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mOtherRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
         mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
@@ -557,13 +557,14 @@
             }
         };
 
-        mDescriptionFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_description_fade_in);
+        Context context = FragmentUtil.getContext(this);
+        mDescriptionFadeInAnimator = loadAnimator(context,
+                R.animator.lb_playback_description_fade_in);
         mDescriptionFadeInAnimator.addUpdateListener(listener);
         mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mDescriptionFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_description_fade_out);
+        mDescriptionFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_description_fade_out);
         mDescriptionFadeOutAnimator.addUpdateListener(listener);
     }
 
@@ -786,6 +787,9 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
         super.onDestroyView();
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
index d41d65f..b4df936 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
@@ -436,11 +436,12 @@
             }
         };
 
-        mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in);
+        Context context = getContext();
+        mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
         mBgFadeInAnimator.addUpdateListener(listener);
         mBgFadeInAnimator.addListener(mFadeListener);
 
-        mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out);
+        mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
         mBgFadeOutAnimator.addUpdateListener(listener);
         mBgFadeOutAnimator.addListener(mFadeListener);
     }
@@ -482,14 +483,14 @@
             }
         };
 
-        mControlRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = getContext();
+        mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
         mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mControlRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mControlRowFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
         mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
@@ -527,14 +528,13 @@
             }
         };
 
-        mOtherRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = getContext();
+        mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mOtherRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
         mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
@@ -560,13 +560,14 @@
             }
         };
 
-        mDescriptionFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_description_fade_in);
+        Context context = getContext();
+        mDescriptionFadeInAnimator = loadAnimator(context,
+                R.animator.lb_playback_description_fade_in);
         mDescriptionFadeInAnimator.addUpdateListener(listener);
         mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mDescriptionFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_description_fade_out);
+        mDescriptionFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_description_fade_out);
         mDescriptionFadeOutAnimator.addUpdateListener(listener);
     }
 
@@ -789,6 +790,9 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
         super.onDestroyView();
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
index 07701f9..31332645 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -455,11 +455,12 @@
             }
         };
 
-        mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in);
+        Context context = getContext();
+        mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
         mBgFadeInAnimator.addUpdateListener(listener);
         mBgFadeInAnimator.addListener(mFadeListener);
 
-        mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out);
+        mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
         mBgFadeOutAnimator.addUpdateListener(listener);
         mBgFadeOutAnimator.addListener(mFadeListener);
     }
@@ -501,14 +502,14 @@
             }
         };
 
-        mControlRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = getContext();
+        mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
         mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mControlRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mControlRowFadeOutAnimator = loadAnimator(context,
+                R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
         mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
@@ -546,14 +547,13 @@
             }
         };
 
-        mOtherRowFadeInAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_in);
+        Context context = getContext();
+        mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
-        mOtherRowFadeOutAnimator = loadAnimator(
-                getActivity(), R.animator.lb_playback_controls_fade_out);
+        mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
         mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
@@ -811,7 +811,6 @@
     /**
      * This listener is called every time there is a selection in {@link RowsSupportFragment}. This can
      * be used by users to take additional actions such as animations.
-     * @hide
      */
     public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
         mExternalItemSelectedListener = listener;
@@ -839,6 +838,14 @@
         super.onDestroyView();
     }
 
+    @Override
+    public void onDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+        super.onDestroy();
+    }
+
     /**
      * Sets the playback row for the playback controls.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
index 93886f9..d65937c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -391,7 +391,8 @@
         super.onResume();
         mIsPaused = false;
         if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
-            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
+            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
+                    FragmentUtil.getContext(this));
             mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
         }
         if (mPendingStartRecognitionWhenPaused) {
@@ -416,6 +417,16 @@
         super.onDestroy();
     }
 
+    /**
+     * Returns RowsFragment that shows result rows. RowsFragment is initialized after
+     * SearchFragment.onCreateView().
+     *
+     * @return RowsFragment that shows result rows.
+     */
+    public RowsFragment getRowsFragment() {
+        return mRowsFragment;
+    }
+
     private void releaseRecognizer() {
         if (null != mSpeechRecognizer) {
             mSearchBar.setSpeechRecognizer(null);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
index c8a058d..ae4c700 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
@@ -394,7 +394,8 @@
         super.onResume();
         mIsPaused = false;
         if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
-            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
+            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
+                    getContext());
             mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
         }
         if (mPendingStartRecognitionWhenPaused) {
@@ -419,6 +420,16 @@
         super.onDestroy();
     }
 
+    /**
+     * Returns RowsSupportFragment that shows result rows. RowsSupportFragment is initialized after
+     * SearchSupportFragment.onCreateView().
+     *
+     * @return RowsSupportFragment that shows result rows.
+     */
+    public RowsSupportFragment getRowsSupportFragment() {
+        return mRowsSupportFragment;
+    }
+
     private void releaseRecognizer() {
         if (null != mSpeechRecognizer) {
             mSearchBar.setSpeechRecognizer(null);
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 b8f3df2..cfa27df 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -13,9 +13,11 @@
  */
 package android.support.v17.leanback.app;
 
+import android.os.Bundle;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.OnChildLaidOutListener;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -23,8 +25,6 @@
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -223,7 +223,7 @@
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(FragmentUtil.getContext(this),
                 R.transition.lb_vertical_grid_entrance_transition);
     }
 
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 2a87f10..55e079d 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
@@ -16,9 +16,11 @@
  */
 package android.support.v17.leanback.app;
 
+import android.os.Bundle;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
 import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.OnChildLaidOutListener;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -26,8 +28,6 @@
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -226,7 +226,7 @@
 
     @Override
     protected Object createEntranceTransition() {
-        return TransitionHelper.loadTransition(getActivity(),
+        return TransitionHelper.loadTransition(getContext(),
                 R.transition.lb_vertical_grid_entrance_transition);
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
index fa9989e..150e461 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -24,7 +24,6 @@
 /**
  * Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView}
  * and rendering video.
- * @hide
  */
 public class VideoFragment extends PlaybackFragment {
     static final int SURFACE_NOT_CREATED = 0;
@@ -39,7 +38,7 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
-        mVideoSurface = (SurfaceView) getActivity().getLayoutInflater().inflate(
+        mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(this)).inflate(
                 R.layout.lb_video_surface, root, false);
         root.addView(mVideoSurface, 0);
         mVideoSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
@@ -90,4 +89,11 @@
     public SurfaceView getSurfaceView() {
         return mVideoSurface;
     }
+
+    @Override
+    public void onDestroyView() {
+        mVideoSurface = null;
+        mState = SURFACE_NOT_CREATED;
+        super.onDestroyView();
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
index 0b4e34b..ee9a536 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
@@ -13,14 +13,14 @@
  */
 package android.support.v17.leanback.app;
 
+import android.support.v17.leanback.media.PlaybackGlue;
 import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.media.SurfaceHolderGlueHost;
 import android.view.SurfaceHolder;
 
 /**
  * {@link PlaybackGlueHost} implementation
- * the interaction between this class and {@link VideoFragment}.
- * @hide
+ * the interaction between {@link PlaybackGlue} and {@link VideoFragment}.
  */
 public class VideoFragmentGlueHost extends PlaybackFragmentGlueHost
         implements SurfaceHolderGlueHost {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
index 0f76e6e..c12b06f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
@@ -27,7 +27,6 @@
 /**
  * Subclass of {@link PlaybackSupportFragment} that is responsible for providing a {@link SurfaceView}
  * and rendering video.
- * @hide
  */
 public class VideoSupportFragment extends PlaybackSupportFragment {
     static final int SURFACE_NOT_CREATED = 0;
@@ -42,7 +41,7 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
-        mVideoSurface = (SurfaceView) getActivity().getLayoutInflater().inflate(
+        mVideoSurface = (SurfaceView) LayoutInflater.from(getContext()).inflate(
                 R.layout.lb_video_surface, root, false);
         root.addView(mVideoSurface, 0);
         mVideoSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
@@ -93,4 +92,11 @@
     public SurfaceView getSurfaceView() {
         return mVideoSurface;
     }
+
+    @Override
+    public void onDestroyView() {
+        mVideoSurface = null;
+        mState = SURFACE_NOT_CREATED;
+        super.onDestroyView();
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
index 83769ab..67150a0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
@@ -16,14 +16,14 @@
  */
 package android.support.v17.leanback.app;
 
+import android.support.v17.leanback.media.PlaybackGlue;
 import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.media.SurfaceHolderGlueHost;
 import android.view.SurfaceHolder;
 
 /**
  * {@link PlaybackGlueHost} implementation
- * the interaction between this class and {@link VideoSupportFragment}.
- * @hide
+ * the interaction between {@link PlaybackGlue} and {@link VideoSupportFragment}.
  */
 public class VideoSupportFragmentGlueHost extends PlaybackSupportFragmentGlueHost
         implements SurfaceHolderGlueHost {
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java b/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
index 69571b5..75ab391 100644
--- a/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
@@ -21,7 +21,6 @@
  * This class contains the rules for updating the bounds of a
  * {@link CompositeDrawable.ChildDrawable}. It contains four rules, one for each value of the
  * rectangular bound - left/top/right/bottom.
- * @hide
  */
 public class BoundsRule {
     static final int INHERIT_PARENT = 0;
@@ -116,38 +115,38 @@
      * @param result Represents the final bounds.
      */
     public void calculateBounds(Rect rect, Rect result) {
-        if (mLeft == null) {
+        if (left == null) {
             result.left = rect.left;
         } else {
-            result.left = doCalculate(rect.left, mLeft, rect.width());
+            result.left = doCalculate(rect.left, left, rect.width());
         }
 
-        if (mRight == null) {
+        if (right == null) {
             result.right = rect.right;
         } else {
-            result.right = doCalculate(rect.left, mRight, rect.width());
+            result.right = doCalculate(rect.left, right, rect.width());
         }
 
-        if (mTop == null) {
+        if (top == null) {
             result.top = rect.top;
         } else {
-            result.top = doCalculate(rect.top, mTop, rect.height());
+            result.top = doCalculate(rect.top, top, rect.height());
         }
 
-        if (mBottom == null) {
+        if (bottom == null) {
             result.bottom = rect.bottom;
         } else {
-            result.bottom = doCalculate(rect.top, mBottom, rect.height());
+            result.bottom = doCalculate(rect.top, bottom, rect.height());
         }
     }
 
     public BoundsRule() {}
 
     public BoundsRule(BoundsRule boundsRule) {
-        this.mLeft = boundsRule.mLeft != null ? new ValueRule(boundsRule.mLeft) : null;
-        this.mRight = boundsRule.mRight != null ? new ValueRule(boundsRule.mRight) : null;
-        this.mTop = boundsRule.mTop != null ? new ValueRule(boundsRule.mTop) : null;
-        this.mBottom = boundsRule.mBottom != null ? new ValueRule(boundsRule.mBottom) : null;
+        this.left = boundsRule.left != null ? new ValueRule(boundsRule.left) : null;
+        this.right = boundsRule.right != null ? new ValueRule(boundsRule.right) : null;
+        this.top = boundsRule.top != null ? new ValueRule(boundsRule.top) : null;
+        this.bottom = boundsRule.bottom != null ? new ValueRule(boundsRule.bottom) : null;
     }
 
     private int doCalculate(int value, ValueRule rule, int size) {
@@ -165,14 +164,14 @@
     }
 
     /** {@link ValueRule} for left attribute of {@link BoundsRule} */
-    public ValueRule mLeft;
+    public ValueRule left;
 
     /** {@link ValueRule} for top attribute of {@link BoundsRule} */
-    public ValueRule mTop;
+    public ValueRule top;
 
     /** {@link ValueRule} for right attribute of {@link BoundsRule} */
-    public ValueRule mRight;
+    public ValueRule right;
 
     /** {@link ValueRule} for bottom attribute of {@link BoundsRule} */
-    public ValueRule mBottom;
+    public ValueRule bottom;
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java b/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
index d27a984..b7f77bd 100644
--- a/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
@@ -32,9 +32,7 @@
 /**
  * Generic drawable class that can be composed of multiple children. Whenever the bounds changes
  * for this class, it updates those of its children.
- * @hide
  */
-@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
 public class CompositeDrawable extends Drawable implements Drawable.Callback {
 
     static class CompositeState extends Drawable.ConstantState {
@@ -106,6 +104,13 @@
     }
 
     /**
+     * Sets the supplied region at given index.
+     */
+    public void setChildDrawableAt(int index, Drawable drawable) {
+        mState.mChildren.set(index, new ChildDrawable(drawable, this));
+    }
+
+    /**
      * Returns the {@link Drawable} for the given index.
      */
     public Drawable getDrawable(int index) {
@@ -313,10 +318,10 @@
                         Integer.class, "absoluteTop") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
-                if (obj.getBoundsRule().mTop == null) {
-                    obj.getBoundsRule().mTop = BoundsRule.absoluteValue(value);
+                if (obj.getBoundsRule().top == null) {
+                    obj.getBoundsRule().top = BoundsRule.absoluteValue(value);
                 } else {
-                    obj.getBoundsRule().mTop.setAbsoluteValue(value);
+                    obj.getBoundsRule().top.setAbsoluteValue(value);
                 }
 
                 obj.recomputeBounds();
@@ -324,10 +329,10 @@
 
             @Override
             public Integer get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mTop == null) {
+                if (obj.getBoundsRule().top == null) {
                     return obj.mParent.getBounds().top;
                 }
-                return obj.getBoundsRule().mTop.getAbsoluteValue();
+                return obj.getBoundsRule().top.getAbsoluteValue();
             }
         };
 
@@ -339,10 +344,10 @@
                         Integer.class, "absoluteBottom") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
-                if (obj.getBoundsRule().mBottom == null) {
-                    obj.getBoundsRule().mBottom = BoundsRule.absoluteValue(value);
+                if (obj.getBoundsRule().bottom == null) {
+                    obj.getBoundsRule().bottom = BoundsRule.absoluteValue(value);
                 } else {
-                    obj.getBoundsRule().mBottom.setAbsoluteValue(value);
+                    obj.getBoundsRule().bottom.setAbsoluteValue(value);
                 }
 
                 obj.recomputeBounds();
@@ -350,10 +355,10 @@
 
             @Override
             public Integer get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mBottom == null) {
+                if (obj.getBoundsRule().bottom == null) {
                     return obj.mParent.getBounds().bottom;
                 }
-                return obj.getBoundsRule().mBottom.getAbsoluteValue();
+                return obj.getBoundsRule().bottom.getAbsoluteValue();
             }
         };
 
@@ -366,10 +371,10 @@
                         Integer.class, "absoluteLeft") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
-                if (obj.getBoundsRule().mLeft == null) {
-                    obj.getBoundsRule().mLeft = BoundsRule.absoluteValue(value);
+                if (obj.getBoundsRule().left == null) {
+                    obj.getBoundsRule().left = BoundsRule.absoluteValue(value);
                 } else {
-                    obj.getBoundsRule().mLeft.setAbsoluteValue(value);
+                    obj.getBoundsRule().left.setAbsoluteValue(value);
                 }
 
                 obj.recomputeBounds();
@@ -377,10 +382,10 @@
 
             @Override
             public Integer get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mLeft == null) {
+                if (obj.getBoundsRule().left == null) {
                     return obj.mParent.getBounds().left;
                 }
-                return obj.getBoundsRule().mLeft.getAbsoluteValue();
+                return obj.getBoundsRule().left.getAbsoluteValue();
             }
         };
 
@@ -392,10 +397,10 @@
                         Integer.class, "absoluteRight") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
-                if (obj.getBoundsRule().mRight == null) {
-                    obj.getBoundsRule().mRight = BoundsRule.absoluteValue(value);
+                if (obj.getBoundsRule().right == null) {
+                    obj.getBoundsRule().right = BoundsRule.absoluteValue(value);
                 } else {
-                    obj.getBoundsRule().mRight.setAbsoluteValue(value);
+                    obj.getBoundsRule().right.setAbsoluteValue(value);
                 }
 
                 obj.recomputeBounds();
@@ -403,10 +408,10 @@
 
             @Override
             public Integer get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mRight == null) {
+                if (obj.getBoundsRule().right == null) {
                     return obj.mParent.getBounds().right;
                 }
-                return obj.getBoundsRule().mRight.getAbsoluteValue();
+                return obj.getBoundsRule().right.getAbsoluteValue();
             }
         };
 
@@ -421,10 +426,10 @@
                 new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractionTop") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Float value) {
-                if (obj.getBoundsRule().mTop == null) {
-                    obj.getBoundsRule().mTop = BoundsRule.inheritFromParent(value);
+                if (obj.getBoundsRule().top == null) {
+                    obj.getBoundsRule().top = BoundsRule.inheritFromParent(value);
                 } else {
-                    obj.getBoundsRule().mTop.setFraction(value);
+                    obj.getBoundsRule().top.setFraction(value);
                 }
 
                 obj.recomputeBounds();
@@ -432,10 +437,10 @@
 
             @Override
             public Float get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mTop == null) {
+                if (obj.getBoundsRule().top == null) {
                     return 0f;
                 }
-                return obj.getBoundsRule().mTop.getFraction();
+                return obj.getBoundsRule().top.getFraction();
             }
         };
 
@@ -451,10 +456,10 @@
                         Float.class, "fractionBottom") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Float value) {
-                if (obj.getBoundsRule().mBottom == null) {
-                    obj.getBoundsRule().mBottom = BoundsRule.inheritFromParent(value);
+                if (obj.getBoundsRule().bottom == null) {
+                    obj.getBoundsRule().bottom = BoundsRule.inheritFromParent(value);
                 } else {
-                    obj.getBoundsRule().mBottom.setFraction(value);
+                    obj.getBoundsRule().bottom.setFraction(value);
                 }
 
                 obj.recomputeBounds();
@@ -462,10 +467,10 @@
 
             @Override
             public Float get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mBottom == null) {
+                if (obj.getBoundsRule().bottom == null) {
                     return 1f;
                 }
-                return obj.getBoundsRule().mBottom.getFraction();
+                return obj.getBoundsRule().bottom.getFraction();
             }
         };
 
@@ -480,10 +485,10 @@
                 new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractionLeft") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Float value) {
-                if (obj.getBoundsRule().mLeft == null) {
-                    obj.getBoundsRule().mLeft = BoundsRule.inheritFromParent(value);
+                if (obj.getBoundsRule().left == null) {
+                    obj.getBoundsRule().left = BoundsRule.inheritFromParent(value);
                 } else {
-                    obj.getBoundsRule().mLeft.setFraction(value);
+                    obj.getBoundsRule().left.setFraction(value);
                 }
 
                 obj.recomputeBounds();
@@ -491,10 +496,10 @@
 
             @Override
             public Float get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mLeft == null) {
+                if (obj.getBoundsRule().left == null) {
                     return 0f;
                 }
-                return obj.getBoundsRule().mLeft.getFraction();
+                return obj.getBoundsRule().left.getFraction();
             }
         };
 
@@ -509,10 +514,10 @@
                 new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractoinRight") {
             @Override
             public void set(CompositeDrawable.ChildDrawable obj, Float value) {
-                if (obj.getBoundsRule().mRight == null) {
-                    obj.getBoundsRule().mRight = BoundsRule.inheritFromParent(value);
+                if (obj.getBoundsRule().right == null) {
+                    obj.getBoundsRule().right = BoundsRule.inheritFromParent(value);
                 } else {
-                    obj.getBoundsRule().mRight.setFraction(value);
+                    obj.getBoundsRule().right.setFraction(value);
                 }
 
                 obj.recomputeBounds();
@@ -520,10 +525,10 @@
 
             @Override
             public Float get(CompositeDrawable.ChildDrawable obj) {
-                if (obj.getBoundsRule().mRight == null) {
+                if (obj.getBoundsRule().right == null) {
                     return 1f;
                 }
-                return obj.getBoundsRule().mRight.getFraction();
+                return obj.getBoundsRule().right.getFraction();
             }
         };
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java b/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
index 4641178..910f313 100644
--- a/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
@@ -15,6 +15,7 @@
  */
 package android.support.v17.leanback.graphics;
 
+import android.annotation.TargetApi;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
@@ -22,14 +23,17 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.support.annotation.NonNull;
+import android.util.IntProperty;
+import android.util.Property;
 
 /**
  * Subclass of {@link Drawable} that can be used to draw a bitmap into a region. Bitmap
  * will be scaled to fit the full width of the region and will be aligned to the top left corner.
  * Any region outside the bounds will be clipped during {@link #draw(Canvas)} call. Top
- * position of the bitmap can be controlled by {@link #setVerticalOffset(int)} call.
- * @hide
+ * position of the bitmap can be controlled by {@link #setVerticalOffset(int)} call or
+ * {@link #PROPERTY_VERTICAL_OFFSET}.
  */
 public class FitWidthBitmapDrawable extends Drawable {
 
@@ -127,6 +131,7 @@
     /**
      * Sets the vertical offset which will be used for drawing the bitmap. The bitmap drawing
      * will start the provided vertical offset.
+     * @see #PROPERTY_VERTICAL_OFFSET
      */
     public void setVerticalOffset(int offset) {
         mBitmapState.mOffset = offset;
@@ -135,6 +140,7 @@
 
     /**
      * Returns the current vertical offset.
+     * @see #PROPERTY_VERTICAL_OFFSET
      */
     public int getVerticalOffset() {
         return mBitmapState.mOffset;
@@ -194,4 +200,42 @@
             return mBitmapState.mSource;
         }
     }
+
+    /**
+     * Property for {@link #setVerticalOffset(int)} and {@link #getVerticalOffset()}.
+     */
+    public static final Property<FitWidthBitmapDrawable, Integer> PROPERTY_VERTICAL_OFFSET;
+
+    static {
+        if (Build.VERSION.SDK_INT >= 24) {
+            // use IntProperty
+            PROPERTY_VERTICAL_OFFSET = getVerticalOffsetIntProperty();
+        } else {
+            // use Property
+            PROPERTY_VERTICAL_OFFSET = new Property<FitWidthBitmapDrawable, Integer>(Integer.class,
+                    "verticalOffset") {
+                public void set(FitWidthBitmapDrawable object, Integer value) {
+                    object.setVerticalOffset(value);
+                }
+
+                public Integer get(FitWidthBitmapDrawable object) {
+                    return object.getVerticalOffset();
+                }
+            };
+        }
+    }
+
+    @TargetApi(24)
+    static IntProperty<FitWidthBitmapDrawable> getVerticalOffsetIntProperty() {
+        return new IntProperty<FitWidthBitmapDrawable>("verticalOffset") {
+            public void setValue(FitWidthBitmapDrawable fitWidthBitmapDrawable, int value) {
+                fitWidthBitmapDrawable.setVerticalOffset(value);
+            }
+
+            @Override
+            public Integer get(FitWidthBitmapDrawable fitWidthBitmapDrawable) {
+                return fitWidthBitmapDrawable.getVerticalOffset();
+            }
+        };
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
index abee2dc..b0f0b8a 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
@@ -29,7 +29,6 @@
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.View;
@@ -159,14 +158,18 @@
      * Release internal MediaPlayer. Should not use the object after call release().
      */
     public void release() {
+        mInitialized = false;
         mPlayer.release();
     }
 
     @Override
     protected void onDetachedFromHost() {
-        super.onDetachedFromHost();
+        if (getHost() instanceof SurfaceHolderGlueHost) {
+            ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
+        }
         reset();
         release();
+        super.onDetachedFromHost();
     }
 
     @Override
@@ -185,18 +188,19 @@
 
     @Override
     public void enableProgressUpdating(final boolean enabled) {
+        if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
         if (!enabled) {
-            if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
             return;
         }
-        mRunnable = new Runnable() {
-            @Override
-            public void run() {
-                updateProgress();
-                Log.d(TAG, "enableProgressUpdating(boolean)");
-                mHandler.postDelayed(this, getUpdatePeriod());
-            }
-        };
+        if (mRunnable == null) {
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    updateProgress();
+                    mHandler.postDelayed(this, getUpdatePeriod());
+                }
+            };
+        }
         mHandler.postDelayed(mRunnable, getUpdatePeriod());
     }
 
@@ -262,7 +266,7 @@
 
     @Override
     public boolean isMediaPlaying() {
-        return mPlayer.isPlaying();
+        return mInitialized && mPlayer.isPlaying();
     }
 
     @Override
@@ -295,7 +299,7 @@
     @Override
     public int getCurrentSpeedId() {
         // 0 = Pause, 1 = Normal Playback Speed
-        return mPlayer.isPlaying() ? 1 : 0;
+        return isMediaPlaying() ? 1 : 0;
     }
 
     @Override
@@ -305,6 +309,9 @@
 
     @Override
     public void play(int speed) {
+        if (!mInitialized || mPlayer.isPlaying()) {
+            return;
+        }
         mPlayer.start();
         onMetadataChanged();
         onStateChanged();
@@ -313,8 +320,9 @@
 
     @Override
     public void pause() {
-        if (mPlayer.isPlaying()) {
+        if (isMediaPlaying()) {
             mPlayer.pause();
+            onStateChanged();
         }
     }
 
@@ -359,6 +367,9 @@
      * @param newPosition The new position of the media track in milliseconds.
      */
     protected void seekTo(int newPosition) {
+        if (!mInitialized) {
+            return;
+        }
         mPlayer.seekTo(newPosition);
     }
 
@@ -379,6 +390,7 @@
             prepareMediaForPlaying();
         } else {
             mMediaSourceUri = uri;
+            prepareMediaForPlaying();
         }
         return true;
     }
@@ -400,6 +412,7 @@
             prepareMediaForPlaying();
         } else {
             mMediaSourcePath = path;
+            prepareMediaForPlaying();
         }
         return true;
     }
@@ -436,6 +449,9 @@
         mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
             @Override
             public void onBufferingUpdate(MediaPlayer mp, int percent) {
+                if (getControlsRow() == null) {
+                    return;
+                }
                 getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
             }
         });
@@ -475,15 +491,9 @@
      * {@link PlaybackGlueHost}.
      */
     class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
-        private boolean mMediaPlayerReset = true;
-
         @Override
         public void surfaceCreated(SurfaceHolder surfaceHolder) {
-            if (mMediaPlayerReset) {
-                mMediaPlayerReset = false;
-                setDisplay(surfaceHolder);
-                prepareMediaForPlaying();
-            }
+            setDisplay(surfaceHolder);
         }
 
         @Override
@@ -492,9 +502,7 @@
 
         @Override
         public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
-            reset();
             setDisplay(null);
-            mMediaPlayerReset = true;
         }
     }
 }
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 441de12..215ba22 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -254,6 +254,12 @@
         enableProgressUpdating(false);
     }
 
+    @Override
+    protected void onDetachedFromHost() {
+        enableProgressUpdating(false);
+        super.onDetachedFromHost();
+    }
+
     /**
      * Instantiating a {@link PlaybackControlsRow} and corresponding
      * {@link PlaybackControlsRowPresenter}. Subclass may override.
@@ -393,7 +399,9 @@
     public void updateProgress() {
         int position = getCurrentPosition();
         if (DEBUG) Log.v(TAG, "updateProgress " + position);
-        mControlsRow.setCurrentTime(position);
+        if (mControlsRow != null) {
+            mControlsRow.setCurrentTime(position);
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
index 8ed4b93..3f55da3 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
@@ -21,10 +21,19 @@
 
 /**
  * Base class for abstraction of media play/pause feature. A subclass of PlaybackGlue will contain
- * implementation of Media Player. App initializes PlaybackGlue subclass, associated it with a
- * {@link PlaybackGlueHost}. {@link PlaybackGlueHost} is typically implemented by a Fragment or
- * an Activity, it provides the environment to render UI for PlaybackGlue object, it optionally
- * provides SurfaceHolder via {@link SurfaceHolderGlueHost} to render video.
+ * implementation of Media Player or a connection to playback Service. App initializes
+ * PlaybackGlue subclass, associated it with a {@link PlaybackGlueHost}. {@link PlaybackGlueHost}
+ * is typically implemented by a Fragment or an Activity, it provides the environment to render UI
+ * for PlaybackGlue object, it optionally provides SurfaceHolder via {@link SurfaceHolderGlueHost}
+ * to render video. A typical PlaybackGlue should release resources (e.g. MediaPlayer or connection
+ * to playback Service) in {@link #onDetachedFromHost()}.
+ * {@link #onDetachedFromHost()} is called in two cases:
+ * <ul>
+ * <li> app manually change it using {@link #setHost(PlaybackGlueHost)} call</li>
+ * <li> When host (fragment or activity) is destroyed </li>
+ * </ul>
+ * In rare case if an PlaybackGlue wants to live outside fragment / activity life cycle, it may
+ * manages resource release by itself.
  *
  * @see PlaybackGlueHost
  */
@@ -58,7 +67,10 @@
 
     /**
      * Returns true when the media player is ready to start media playback. Subclasses must
-     * implement this method correctly.
+     * implement this method correctly. When returning false, app may listen to
+     * {@link PlayerCallback#onReadyForPlayback()} event.
+     *
+     * @see PlayerCallback#onReadyForPlayback()
      */
     public boolean isReadyForPlayback() {
         return true;
@@ -95,7 +107,10 @@
     }
 
     /**
-     * This method is used to configure the {@link PlaybackGlueHost} with required listeners.
+     * This method is used to associate a PlaybackGlue with the {@link PlaybackGlueHost} which
+     * provides UI and optional {@link SurfaceHolderGlueHost}.
+     *
+     * @param host The host for the PlaybackGlue. Set to null to detach from the host.
      */
     public final void setHost(PlaybackGlueHost host) {
         if (mPlaybackGlueHost == host) {
@@ -161,13 +176,21 @@
             public void onHostPause() {
                 PlaybackGlue.this.onHostPause();
             }
+
+            @Override
+            public void onHostDestroy() {
+                if (mPlaybackGlueHost != null) {
+                    mPlaybackGlueHost.attachToGlue(null);
+                }
+            }
         });
     }
 
     /**
      * This method is called when current associated {@link PlaybackGlueHost} is attached to a
-     * different {@link PlaybackGlue}. Subclass may override and call super.onDetachedFromHost()
-     * at last.
+     * different {@link PlaybackGlue} or {@link PlaybackGlueHost} is destroyed . Subclass may
+     * override and call super.onDetachedFromHost() at last. A typical PlaybackGlue will release
+     * resources (e.g. MediaPlayer or connection to playback service) in this method.
      */
     @CallSuper
     protected void onDetachedFromHost() {
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
index 3cf086b..799074c 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
@@ -72,6 +72,12 @@
          */
         public void onHostResume() {
         }
+
+        /**
+         * Callback triggered once the host(fragment) has been destroyed.
+         */
+        public void onHostDestroy() {
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java b/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
index 03af625..25be4c6 100644
--- a/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
+++ b/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
@@ -25,7 +25,7 @@
 import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
 import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.ParallaxSource;
+import android.support.v17.leanback.widget.Parallax;
 import android.transition.TransitionValues;
 import android.transition.Visibility;
 import android.util.AttributeSet;
@@ -57,7 +57,7 @@
     }
 
     Animator createAnimator(View view) {
-        final ParallaxSource<?> source = (ParallaxSource) view.getTag(R.id.lb_parallax_source);
+        final Parallax<?> source = (Parallax) view.getTag(R.id.lb_parallax_source);
         if (source == null) {
             return null;
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index 2d007e8..005a441 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -27,7 +27,6 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.animation.AccelerateDecelerateInterpolator;
 
 /**
  * An abstract base class for vertically and horizontally scrolling lists. The items come
@@ -195,6 +194,11 @@
     RecyclerView.RecyclerListener mChainedRecyclerListener;
     private OnUnhandledKeyListener mOnUnhandledKeyListener;
 
+    /**
+     * Number of items to prefetch when first coming on screen with new data.
+     */
+    int mInitialItemPrefetchCount = 4;
+
     public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mLayoutManager = new GridLayoutManager(this);
@@ -998,21 +1002,70 @@
         return mLayoutManager.getExtraLayoutSpace();
     }
 
-
+    /**
+     * Temporarily slide out child views to bottom (for VerticalGridView) or end
+     * (for HorizontalGridView). The views will be automatically slide-in in next
+     * {@link #smoothScrollToPosition(int)} or {@link #scrollToPosition(int)}.
+     */
     public void animateOut() {
-        ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(true);
-        smoothScrollBy(0, -600, new AccelerateDecelerateInterpolator());
+        mLayoutManager.slideOut();
     }
 
+    /**
+     * @deprecated No longer needed. Children being slide out by {@link #animateOut()} will be
+     * slide in next focus or (smooth)scrollToPosition action.
+     */
+    @Deprecated
     public void animateIn() {
-        addOnScrollListener(new OnScrollListener() {
-            @Override
-            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
-                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
-                    ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(false);
-                }
-            }
-        });
-        smoothScrollBy(0, 600, new AccelerateDecelerateInterpolator());
+    }
+
+
+    /**
+     * Sets the number of items to prefetch in
+     * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
+     * which defines how many inner items should be prefetched when this GridView is nested inside
+     * another RecyclerView.
+     *
+     * <p>Set this value to the number of items this inner GridView will display when it is
+     * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items
+     * so they are ready, avoiding jank as the inner GridView is scrolled into the viewport.</p>
+     *
+     * <p>For example, take a VerticalGridView of scrolling HorizontalGridViews. The rows always
+     * have 6 items visible in them (or 7 if not aligned). Passing <code>6</code> to this method
+     * for each inner GridView will enable RecyclerView's prefetching feature to do create/bind work
+     * for 6 views within a row early, before it is scrolled on screen, instead of just the default
+     * 4.</p>
+     *
+     * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView
+     * nested in another RecyclerView.</p>
+     *
+     * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of
+     * views that will be visible in this view can incur unnecessary bind work, and an increase to
+     * the number of Views created and in active use.</p>
+     *
+     * @param itemCount Number of items to prefetch
+     *
+     * @see #getInitialItemPrefetchCount()
+     * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
+     * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
+     */
+    public void setInitialPrefetchItemCount(int itemCount) {
+        mInitialItemPrefetchCount = itemCount;
+    }
+
+    /**
+     * Gets the number of items to prefetch in
+     * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
+     * which defines how many inner items should be prefetched when this GridView is nested inside
+     * another RecyclerView.
+     *
+     * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
+     * @see #setInitialPrefetchItemCount(int)
+     * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
+     *
+     * @return number of items to prefetch.
+     */
+    public int getInitialItemPrefetchCount() {
+        return mInitialItemPrefetchCount;
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java b/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
index 7f90cf6..43c4ffb 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
@@ -44,6 +44,8 @@
         /**
          * See {@link android.view.ViewGroup#onRequestFocusInDescendants(
          * int, android.graphics.Rect)}.
+         * @return True if handled by listener, otherwise returns {@link
+         * android.view.ViewGroup#onRequestFocusInDescendants(int, android.graphics.Rect)}.
          */
         boolean onRequestFocusInDescendants(int direction,
                 Rect previouslyFocusedRect);
@@ -102,8 +104,10 @@
     protected boolean onRequestFocusInDescendants(int direction,
             Rect previouslyFocusedRect) {
         if (mOnChildFocusListener != null) {
-            return mOnChildFocusListener.onRequestFocusInDescendants(direction,
-                    previouslyFocusedRect);
+            if (mOnChildFocusListener.onRequestFocusInDescendants(direction,
+                    previouslyFocusedRect)) {
+                return true;
+            }
         }
         return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
     }
@@ -121,10 +125,10 @@
 
     @Override
     public void requestChildFocus(View child, View focused) {
-        super.requestChildFocus(child, focused);
         if (mOnChildFocusListener != null) {
             mOnChildFocusListener.onRequestChildFocus(child, focused);
         }
+        super.requestChildFocus(child, focused);
     }
 
     @Override
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
new file mode 100644
index 0000000..374485d
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.app.DetailsSupportFragment;
+
+/**
+ * Subclass of Parallax object that tracks overview row's top and bottom edge in DetailsFragment
+ * or DetailsSupportFragment.
+ * <p>
+ * It can be used for both creating cover image parallax effect and controlling video playing
+ * when transitioning to/from half/full screen.  A direct use case is
+ * {@link android.support.v17.leanback.app.DetailsFragmentBackgroundController}.
+ * </p>
+ * @see DetailsFragment#getParallax()
+ * @see android.support.v17.leanback.app.DetailsFragmentBackgroundController
+ * @see DetailsSupportFragment#getParallax()
+ * @see android.support.v17.leanback.app.DetailsSupportFragmentBackgroundController
+ */
+public class DetailsParallax extends RecyclerViewParallax {
+    final Parallax.IntProperty mFrameTop;
+    final Parallax.IntProperty mFrameBottom;
+
+    public DetailsParallax() {
+        // track the top edge of details_frame of first item of adapter
+        mFrameTop = this
+                .addProperty("overviewRowTop")
+                .adapterPosition(0)
+                .viewId(R.id.details_frame);
+
+        // track the bottom edge of details_frame of first item of adapter
+        mFrameBottom = this
+                .addProperty("overviewRowBottom")
+                .adapterPosition(0)
+                .viewId(R.id.details_frame)
+                .fraction(1.0f);
+
+    }
+
+    /**
+     * Returns the top of the details overview row. This is tracked for implementing the
+     * parallax effect.
+     */
+    public Parallax.IntProperty getOverviewRowTop() {
+        return mFrameTop;
+    }
+
+    /**
+     * Returns the bottom of the details overview row. This is tracked for implementing the
+     * parallax effect.
+     */
+    public Parallax.IntProperty getOverviewRowBottom() {
+        return mFrameBottom;
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
new file mode 100644
index 0000000..d77bc44
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.BoundsRule;
+import android.support.v17.leanback.graphics.CompositeDrawable;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.util.TypedValue;
+
+/**
+ * Helper class responsible for wiring in parallax effect in
+ * {@link android.support.v17.leanback.app.DetailsFragment}. The default effect will render
+ * a drawable like the following two parts, cover drawable above DetailsOverviewRow and solid
+ * color below DetailsOverviewRow.
+ * <pre>
+ *        ***************************
+ *        *        Cover Drawable   *
+ *        ***************************
+ *        *    DetailsOverviewRow   *
+ *        *                         *
+ *        ***************************
+ *        *     Bottom Drawable     *
+ *        *      (Solid Color)      *
+ *        *         Related         *
+ *        *         Content         *
+ *        ***************************
+ * </pre>
+ * <ul>
+ * <li>
+ * Call {@link #DetailsParallaxDrawable(Context, DetailsParallax)} to create DetailsParallaxDrawable
+ * using {@link FitWidthBitmapDrawable} for cover drawable.
+ * </li>
+ * </ul>
+ * <li>
+ * In case the solid color is not set, it will use defaultBrandColorDark from LeanbackTheme.
+ * </li>
+ * @hide
+ */
+public class DetailsParallaxDrawable extends CompositeDrawable {
+    private Drawable mBottomDrawable;
+
+    /**
+     * Creates a DetailsParallaxDrawable using a cover drawable.
+     * @param context Context to get resource values.
+     * @param parallax DetailsParallax to add background parallax effect.
+     * @param coverDrawable Cover drawable at top
+     * @param coverDrawableParallaxTarget Define a ParallaxTarget that would be performed on cover
+     *                                    Drawable. e.g. To change "verticalOffset" of cover
+     *                                    Drawable from 0 to 120 pixels above screen, uses:
+     *                                    new ParallaxTarget.PropertyValuesHolderTarget(
+     *                                        coverDrawable,
+     *                                        PropertyValuesHolder.ofInt("verticalOffset", 0, -120))
+     */
+    public DetailsParallaxDrawable(Context context, DetailsParallax parallax,
+                                   Drawable coverDrawable,
+                                   ParallaxTarget coverDrawableParallaxTarget) {
+        init(context, parallax, coverDrawable, new ColorDrawable(), coverDrawableParallaxTarget);
+    }
+
+    /**
+     * Creates a DetailsParallaxDrawable using a cover drawable and bottom drawable.
+     * @param context Context to get resource values.
+     * @param parallax DetailsParallax to add background parallax effect.
+     * @param coverDrawable Cover drawable at top
+     * @param bottomDrawable Bottom drawable, when null it will create a default ColorDrawable.
+     * @param coverDrawableParallaxTarget Define a ParallaxTarget that would be performed on cover
+     *                                    Drawable. e.g. To change "verticalOffset" of cover
+     *                                    Drawable from 0 to 120 pixels above screen, uses:
+     *                                    new ParallaxTarget.PropertyValuesHolderTarget(
+     *                                        coverDrawable,
+     *                                        PropertyValuesHolder.ofInt("verticalOffset", 0, -120))
+     */
+    public DetailsParallaxDrawable(Context context, DetailsParallax parallax,
+                                   Drawable coverDrawable, Drawable bottomDrawable,
+                                   ParallaxTarget coverDrawableParallaxTarget) {
+
+        init(context, parallax, coverDrawable, bottomDrawable, coverDrawableParallaxTarget);
+    }
+
+    /**
+     * Creates DetailsParallaxDrawable using {@link FitWidthBitmapDrawable} for cover drawable.
+     * @param context Context to get resource values.
+     * @param parallax DetailsParallax to add background parallax effect.
+     */
+    public DetailsParallaxDrawable(Context context, DetailsParallax parallax) {
+        int verticalMovementMax = -context.getResources().getDimensionPixelSize(
+                R.dimen.lb_details_cover_drawable_parallax_movement);
+        Drawable coverDrawable = new FitWidthBitmapDrawable();
+        ParallaxTarget coverDrawableParallaxTarget = new ParallaxTarget.PropertyValuesHolderTarget(
+                coverDrawable, PropertyValuesHolder.ofInt("verticalOffset", 0,
+                verticalMovementMax));
+        init(context, parallax, coverDrawable, new ColorDrawable(), coverDrawableParallaxTarget);
+    }
+
+    void init(Context context, DetailsParallax parallax,
+              Drawable coverDrawable, Drawable bottomDrawable,
+              ParallaxTarget coverDrawableParallaxTarget) {
+        if (bottomDrawable instanceof ColorDrawable) {
+            ColorDrawable colorDrawable = ((ColorDrawable) bottomDrawable);
+            if (colorDrawable.getColor() == Color.TRANSPARENT) {
+                colorDrawable.setColor(getDefaultBackgroundColor(context));
+            }
+        }
+        addChildDrawable(coverDrawable);
+        addChildDrawable(mBottomDrawable = bottomDrawable);
+        getChildAt(0).getBoundsRule().bottom = BoundsRule.inheritFromParent(1f);
+        getChildAt(1).getBoundsRule().top = BoundsRule.inheritFromParent(1f);
+        connect(context, parallax, coverDrawableParallaxTarget);
+    }
+
+    private static int getDefaultBackgroundColor(Context context) {
+        TypedValue outValue = new TypedValue();
+        if (context.getTheme().resolveAttribute(R.attr.defaultBrandColorDark, outValue, true)) {
+            return context.getResources().getColor(outValue.resourceId);
+        }
+        return context.getResources().getColor(R.color.lb_default_brand_color_dark);
+    }
+
+    /**
+     * @return First child which is cover drawable appearing at top.
+     */
+    public Drawable getCoverDrawable() {
+        return getChildAt(0).getDrawable();
+    }
+
+    /**
+     * @return Second child which is ColorDrawable by default.
+     */
+    public Drawable getBottomDrawable() {
+        return mBottomDrawable;
+    }
+
+    /**
+     * Changes the solid background color of the related content section.
+     */
+    public void setSolidColor(@ColorInt int color) {
+        ((ColorDrawable) mBottomDrawable).setColor(color);
+    }
+
+    /**
+     * @return Returns the solid background color of the related content section.
+     */
+    public @ColorInt int getSolidColor() {
+        return ((ColorDrawable) mBottomDrawable).getColor();
+    }
+
+    /**
+     * Connects DetailsParallaxDrawable to DetailsParallax object.
+     * @param parallax The DetailsParallax object to add ParallaxEffects for the drawable.
+     */
+    void connect(Context context, DetailsParallax parallax,
+                        ParallaxTarget coverDrawableParallaxTarget) {
+
+        Parallax.IntProperty frameTop = parallax.getOverviewRowTop();
+        Parallax.IntProperty frameBottom = parallax.getOverviewRowBottom();
+
+        final int fromValue = context.getResources()
+                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions);
+        final int toValue = context.getResources()
+                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description);
+        parallax
+                .addEffect(frameTop.atAbsolute(fromValue), frameTop.atAbsolute(toValue))
+                .target(coverDrawableParallaxTarget);
+
+        // Add solid color parallax effect:
+        // When frameBottom moves from bottom of the screen to top of the screen,
+        // change solid ColorDrawable's top from bottom of screen to top of the screen.
+        parallax.addEffect(frameBottom.atFraction(1f), frameBottom.atFraction(0f))
+                .target(getChildAt(1),
+                        PropertyValuesHolder.ofFloat(
+                                CompositeDrawable.ChildDrawable.TOP_FRACTION, 1f, 0f));
+        // Also when frameTop moves from bottom of screen to top of the screen,
+        // we are changing bottom of the bitmap from bottom of screen to top of screen.
+        parallax.addEffect(frameTop.atFraction(1f), frameTop.atFraction(0f))
+                .target(getChildAt(0),
+                        PropertyValuesHolder.ofFloat(
+                                CompositeDrawable.ChildDrawable.BOTTOM_FRACTION, 1f, 0f));
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
index 641bfcc..728d31f 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
@@ -23,7 +23,9 @@
 import android.content.res.Resources;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.graphics.ColorOverlayDimmer;
+import android.support.v7.widget.RecyclerView;
 import android.view.View;
+import android.view.ViewParent;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
 
@@ -181,7 +183,9 @@
     }
 
     /**
-     * Sets up the focus highlight behavior of a focused item in browse list row.
+     * Sets up the focus highlight behavior of a focused item in browse list row. App usually does
+     * not call this method, it uses {@link ListRowPresenter#ListRowPresenter(int, boolean)}.
+     *
      * @param zoomIndex One of {@link FocusHighlight#ZOOM_FACTOR_SMALL}
      * {@link FocusHighlight#ZOOM_FACTOR_XSMALL}
      * {@link FocusHighlight#ZOOM_FACTOR_MEDIUM}
@@ -196,34 +200,54 @@
     }
 
     /**
-     * Sets up the focus highlight behavior of a focused item in header list.
-     * @param gridView  the header list.
+     * Sets up default focus highlight behavior of a focused item in header list. It would scale
+     * the focused item and update
+     * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}.
+     * Equivalent to call setupHeaderItemFocusHighlight(gridView, true). This method should be
+     * called after header fragment onViewCreated().
+     *
+     * @param gridView  The header list.
      */
     public static void setupHeaderItemFocusHighlight(VerticalGridView gridView) {
-        if (gridView.getAdapter() instanceof ItemBridgeAdapter) {
+        setupHeaderItemFocusHighlight(gridView, true);
+    }
+
+    /**
+     * Sets up the focus highlight behavior of a focused item in header list. This method should be
+     * called after header fragment onViewCreated().
+     *
+     * @param gridView  The header list.
+     * @param scaleEnabled True if scale the item when focused, false otherwise. Note that
+     * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}
+     * will always be called regardless value of scaleEnabled.
+     */
+    public static void setupHeaderItemFocusHighlight(VerticalGridView gridView,
+                                                     boolean scaleEnabled) {
+        if (gridView != null && gridView.getAdapter() instanceof ItemBridgeAdapter) {
             ((ItemBridgeAdapter) gridView.getAdapter())
-                    .setFocusHighlight(new HeaderItemFocusHighlight(gridView));
+                    .setFocusHighlight(new HeaderItemFocusHighlight(scaleEnabled));
         }
     }
 
     static class HeaderItemFocusHighlight implements FocusHighlightHandler {
-        private static boolean sInitialized;
-        private static float sSelectScale;
-        private static int sDuration;
-        BaseGridView mGridView;
+        private boolean mInitialized;
+        private float mSelectScale;
+        private int mDuration;
+        boolean mScaleEnabled;
 
-        HeaderItemFocusHighlight(BaseGridView gridView) {
-            mGridView = gridView;
-            lazyInit(gridView.getContext().getResources());
+        HeaderItemFocusHighlight(boolean scaleEnabled) {
+            mScaleEnabled = scaleEnabled;
         }
 
-        private static void lazyInit(Resources res) {
-            if (!sInitialized) {
-                sSelectScale =
-                        Float.parseFloat(res.getString(R.dimen.lb_browse_header_select_scale));
-                sDuration =
+        void lazyInit(View view) {
+            if (!mInitialized) {
+                Resources res = view.getResources();
+                mSelectScale = mScaleEnabled
+                        ? Float.parseFloat(res.getString(R.dimen.lb_browse_header_select_scale))
+                        : 1f;
+                mDuration =
                         Integer.parseInt(res.getString(R.dimen.lb_browse_header_select_duration));
-                sInitialized = true;
+                mInitialized = true;
             }
         }
 
@@ -232,7 +256,18 @@
             ItemBridgeAdapter.ViewHolder mViewHolder;
             HeaderFocusAnimator(View view, float scale, int duration) {
                 super(view, scale, false, duration);
-                mViewHolder = (ItemBridgeAdapter.ViewHolder) mGridView.getChildViewHolder(view);
+
+                ViewParent parent = view.getParent();
+                while (parent != null) {
+                    if (parent instanceof RecyclerView) {
+                        break;
+                    }
+                    parent = parent.getParent();
+                }
+                if (parent != null) {
+                    mViewHolder = (ItemBridgeAdapter.ViewHolder) ((RecyclerView) parent)
+                            .getChildViewHolder(view);
+                }
             }
 
             @Override
@@ -248,10 +283,11 @@
         }
 
         private void viewFocused(View view, boolean hasFocus) {
+            lazyInit(view);
             view.setSelected(hasFocus);
             FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
             if (animator == null) {
-                animator = new HeaderFocusAnimator(view, sSelectScale, sDuration);
+                animator = new HeaderFocusAnimator(view, mSelectScale, mDuration);
                 view.setTag(R.id.lb_focus_animator, animator);
             }
             animator.animateFocus(hasFocus, false);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
index 64d151f..eb09225 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
@@ -13,7 +13,10 @@
  */
 package android.support.v17.leanback.widget;
 
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v4.util.CircularIntArray;
+import android.support.v7.widget.RecyclerView;
 
 import java.io.PrintWriter;
 
@@ -259,7 +262,7 @@
      * Finds the largest or smallest row min edge of visible items,
      * the row index is returned in indices[0], the item index is returned in indices[1].
      */
-    public final int findRowMin(boolean findLarge, int[] indices) {
+    public final int findRowMin(boolean findLarge, @Nullable int[] indices) {
         return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex,
                 indices);
     }
@@ -274,7 +277,7 @@
      * Finds the largest or smallest row max edge of visible items, the row index is returned in
      * indices[0], the item index is returned in indices[1].
      */
-    public final int findRowMax(boolean findLarge, int[] indices) {
+    public final int findRowMax(boolean findLarge, @Nullable int[] indices) {
         return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex,
                 indices);
     }
@@ -422,5 +425,12 @@
         }
     }
 
+    /**
+     * Queries items adjacent to the viewport (in the direction of da) into the prefetch registry.
+     */
+    public void collectAdjacentPrefetchPositions(int fromLimit, int da,
+            @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) {
+    }
+
     public abstract void debugPrint(PrintWriter pw);
 }
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 580f01b..5569836 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -42,6 +42,7 @@
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -376,8 +377,8 @@
     // effect smooth scrolling too over to bind an item view then drag the item view back.
     final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
 
-    // Represents whether child views are sliding in or out.
-    private boolean mIsSlidingChildViews;
+    // Represents whether child views are temporarily sliding out
+    boolean mIsSlidingChildViews;
 
     String getTag() {
         return TAG + ":" + mBaseGridView.getId();
@@ -1127,8 +1128,8 @@
             mFocusPosition = 0;
             mSubFocusPosition = 0;
         }
-        if (!mState.didStructureChange() && mGrid.getFirstVisibleIndex() >= 0
-                && !mForceFullLayout && mGrid != null && mGrid.getNumRows() == mNumRows) {
+        if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
+                && !mForceFullLayout && mGrid.getNumRows() == mNumRows) {
             updateScrollController();
             updateScrollSecondAxis();
             mGrid.setSpacing(mSpacingPrimary);
@@ -1334,7 +1335,7 @@
             if (DEBUG) Log.v(getTag(), "request Layout from runnable");
             requestLayout();
         }
-     };
+    };
 
     @Override
     public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
@@ -1374,18 +1375,18 @@
             processRowSizeSecondary(true);
 
             switch (modeSecondary) {
-            case MeasureSpec.UNSPECIFIED:
-                measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
-                break;
-            case MeasureSpec.AT_MOST:
-                measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
-                        mMaxSizeSecondary);
-                break;
-            case MeasureSpec.EXACTLY:
-                measuredSizeSecondary = mMaxSizeSecondary;
-                break;
-            default:
-                throw new IllegalStateException("wrong spec");
+                case MeasureSpec.UNSPECIFIED:
+                    measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
+                    break;
+                case MeasureSpec.AT_MOST:
+                    measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
+                            mMaxSizeSecondary);
+                    break;
+                case MeasureSpec.EXACTLY:
+                    measuredSizeSecondary = mMaxSizeSecondary;
+                    break;
+                default:
+                    throw new IllegalStateException("wrong spec");
             }
 
         } else {
@@ -1395,7 +1396,7 @@
                             ? sizeSecondary - paddingSecondary : mRowSizeSecondaryRequested;
                     mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
                     measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
-                        * (mNumRows - 1) + paddingSecondary;
+                            * (mNumRows - 1) + paddingSecondary;
                     break;
                 case MeasureSpec.AT_MOST:
                 case MeasureSpec.EXACTLY:
@@ -1405,7 +1406,7 @@
                     } else if (mNumRowsRequested == 0) {
                         mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
                         mNumRows = (sizeSecondary + mSpacingSecondary)
-                            / (mRowSizeSecondaryRequested + mSpacingSecondary);
+                                / (mRowSizeSecondaryRequested + mSpacingSecondary);
                     } else if (mRowSizeSecondaryRequested == 0) {
                         mNumRows = mNumRowsRequested;
                         mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary
@@ -1646,7 +1647,7 @@
         final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
         final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary)
                 ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK,
-                        View.LAYOUT_DIRECTION_RTL)
+                View.LAYOUT_DIRECTION_RTL)
                 : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
         if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
                 || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
@@ -1737,8 +1738,41 @@
         return mGrid.appendOneColumnVisibleItems();
     }
 
-    public void setIsSlidingChildViews(boolean animatingChildViews) {
-        this.mIsSlidingChildViews = animatingChildViews;
+    /**
+     * Temporarily slide out child and will be auto slide-in in next scrollToView().
+     */
+    void slideOut() {
+        if (mIsSlidingChildViews) {
+            return;
+        }
+        mIsSlidingChildViews = true;
+        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());
+        } 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());
+        }
     }
 
     private boolean prependOneColumnVisibleItems() {
@@ -2154,6 +2188,40 @@
         return dy;
     }
 
+    @Override
+    public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
+            LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        try {
+            saveContext(null, state);
+            int da = (mOrientation == HORIZONTAL) ? dx : dy;
+            if (getChildCount() == 0 || da == 0) {
+                // can't support this scroll, so don't bother prefetching
+                return;
+            }
+
+            int fromLimit = da < 0
+                    ? -mExtraLayoutSpace
+                    : mSizePrimary + mExtraLayoutSpace;
+            mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry);
+        } finally {
+            leaveContext();
+        }
+    }
+
+    @Override
+    public void collectInitialPrefetchPositions(int adapterItemCount,
+            LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        int numToPrefetch = mBaseGridView.mInitialItemPrefetchCount;
+        if (adapterItemCount != 0 && numToPrefetch != 0) {
+            // prefetch items centered around mFocusPosition
+            int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1)/ 2,
+                    adapterItemCount - numToPrefetch));
+            for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) {
+                layoutPrefetchRegistry.addPosition(i, 0);
+            }
+        }
+    }
+
     void updateScrollMax() {
         int highVisiblePos = (!mReverseFlowPrimary) ? mGrid.getLastVisibleIndex()
                 : mGrid.getFirstVisibleIndex();
@@ -2283,6 +2351,12 @@
         setSelection(position, 0, false, 0);
     }
 
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+            int position) {
+        setSelection(position, 0, true, 0);
+    }
+
     public void setSelection(int position,
             int primaryScrollExtra) {
         setSelection(position, 0, false, primaryScrollExtra);
@@ -2311,7 +2385,7 @@
 
     public void setSelection(int position, int subposition, boolean smooth,
             int primaryScrollExtra) {
-        if (mFocusPosition != position && position != NO_POSITION
+        if (mIsSlidingChildViews || mFocusPosition != position && position != NO_POSITION
                 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
             scrollToSelection(position, subposition, smooth, primaryScrollExtra);
         }
@@ -2422,7 +2496,7 @@
         if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
                 + positionStart + " itemCount " + itemCount);
         if (mFocusPosition != NO_POSITION  && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
-            && mFocusPositionOffset != Integer.MIN_VALUE) {
+                && mFocusPositionOffset != Integer.MIN_VALUE) {
             int pos = mFocusPosition + mFocusPositionOffset;
             if (positionStart <= pos) {
                 if (positionStart + itemCount > pos) {
@@ -2587,6 +2661,7 @@
      * Scroll to a given child view and change mFocusPosition.
      */
     private void scrollToView(View view, View childView, boolean smooth) {
+        mIsSlidingChildViews = false;
         int newFocusPosition = getPositionByView(view);
         int newSubFocusPosition = getSubPositionByView(view, childView);
         if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
@@ -2618,12 +2693,12 @@
 
     boolean getScrollPosition(View view, View childView, int[] deltas) {
         switch (mFocusScrollStrategy) {
-        case BaseGridView.FOCUS_SCROLL_ALIGNED:
-        default:
-            return getAlignedPosition(view, childView, deltas);
-        case BaseGridView.FOCUS_SCROLL_ITEM:
-        case BaseGridView.FOCUS_SCROLL_PAGE:
-            return getNoneAlignedPosition(view, deltas);
+            case BaseGridView.FOCUS_SCROLL_ALIGNED:
+            default:
+                return getAlignedPosition(view, childView, deltas);
+            case BaseGridView.FOCUS_SCROLL_ITEM:
+            case BaseGridView.FOCUS_SCROLL_PAGE:
+                return getNoneAlignedPosition(view, deltas);
         }
     }
 
@@ -2843,6 +2918,10 @@
             return result;
         }
 
+        if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
+            return mBaseGridView.getParent().focusSearch(focused, direction);
+        }
+
         if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction);
         int movement = getMovement(direction);
         final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
@@ -3059,14 +3138,14 @@
     boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
             Rect previouslyFocusedRect) {
         switch (mFocusScrollStrategy) {
-        case BaseGridView.FOCUS_SCROLL_ALIGNED:
-        default:
-            return gridOnRequestFocusInDescendantsAligned(recyclerView,
-                    direction, previouslyFocusedRect);
-        case BaseGridView.FOCUS_SCROLL_PAGE:
-        case BaseGridView.FOCUS_SCROLL_ITEM:
-            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
-                    direction, previouslyFocusedRect);
+            case BaseGridView.FOCUS_SCROLL_ALIGNED:
+            default:
+                return gridOnRequestFocusInDescendantsAligned(recyclerView,
+                        direction, previouslyFocusedRect);
+            case BaseGridView.FOCUS_SCROLL_PAGE:
+            case BaseGridView.FOCUS_SCROLL_ITEM:
+                return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
+                        direction, previouslyFocusedRect);
         }
     }
 
@@ -3137,22 +3216,22 @@
                     movement = NEXT_ROW;
                     break;
             }
-         } else if (mOrientation == VERTICAL) {
-             switch(direction) {
-                 case View.FOCUS_LEFT:
-                     movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW;
-                     break;
-                 case View.FOCUS_RIGHT:
-                     movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW;
-                     break;
-                 case View.FOCUS_UP:
-                     movement = PREV_ITEM;
-                     break;
-                 case View.FOCUS_DOWN:
-                     movement = NEXT_ITEM;
-                     break;
-             }
-         }
+        } else if (mOrientation == VERTICAL) {
+            switch(direction) {
+                case View.FOCUS_LEFT:
+                    movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW;
+                    break;
+                case View.FOCUS_RIGHT:
+                    movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW;
+                    break;
+                case View.FOCUS_UP:
+                    movement = PREV_ITEM;
+                    break;
+                case View.FOCUS_DOWN:
+                    movement = NEXT_ITEM;
+                    break;
+            }
+        }
 
         return movement;
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
index 9540810..e5ff158 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
@@ -963,9 +963,9 @@
 
     void onEditActivatorView(final ViewHolder vh, boolean editing, final boolean withTransition) {
         if (editing) {
+            startExpanded(vh, withTransition);
             vh.itemView.setFocusable(false);
             vh.mActivatorView.requestFocus();
-            startExpanded(vh, withTransition);
             vh.mActivatorView.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
index eba207e..f5dfd99 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
@@ -197,6 +197,15 @@
     }
 
     /**
+     * Changes Presenter that creates and binds the view.
+     * @param presenterSelector Presenter that creates and binds the view.
+     */
+    public void setPresenter(PresenterSelector presenterSelector) {
+        mPresenterSelector = presenterSelector;
+        notifyDataSetChanged();
+    }
+
+    /**
      * Sets the {@link Wrapper}.
      */
     public void setWrapper(Wrapper wrapper) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java b/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java
index c17d4b0..573ca02 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java
@@ -1,73 +1,551 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
+ * 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
+ *      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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
+
 package android.support.v17.leanback.widget;
 
-import java.util.List;
-import java.util.ArrayList;
-
-import android.support.v17.leanback.widget.ParallaxSource.IntPropertyKeyValue;
-import android.support.v17.leanback.widget.ParallaxSource.FloatPropertyKeyValue;
+import android.support.annotation.CallSuper;
 import android.support.v17.leanback.widget.ParallaxEffect.FloatEffect;
 import android.support.v17.leanback.widget.ParallaxEffect.IntEffect;
+import android.util.Property;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
- * Parallax listens to {@link ParallaxSource} changes and invokes performMapping on each
- * {@link ParallaxEffect} object.
- * @hide
+ * Parallax tracks a list of dynamic {@link Property}s typically representing foreground UI
+ * element positions on screen. Parallax keeps a list of {@link ParallaxEffect} objects which define
+ * rules to mapping property values to {@link ParallaxTarget}.
+ *
+ * <p>
+ * There are two types of Parallax, int or float. App should subclass either
+ * {@link Parallax.IntParallax} or {@link Parallax.FloatParallax}. App may subclass
+ * {@link Parallax.IntProperty} or {@link Parallax.FloatProperty} to supply additional information
+ * about how to retrieve Property value.  {@link RecyclerViewParallax} is a great example of
+ * Parallax implementation tracking child view positions on screen.
+ * </p>
+ * <p>
+ * <ul>Restrictions of properties
+ * <li>Values must be in ascending order.</li>
+ * <li>If the UI element is unknown above screen, use UNKNOWN_BEFORE.</li>
+ * <li>if the UI element is unknown below screen, use UNKNOWN_AFTER.</li>
+ * <li>UNKNOWN_BEFORE and UNKNOWN_AFTER are not allowed to be next to each other.</li>
+ * </ul>
+ * These rules can be verified by {@link #verifyProperties()}.
+ * </p>
+ * Subclass should override {@link #updateValues()} to update property values and perform
+ * {@link ParallaxEffect}s. Subclass may call {@link #updateValues()} automatically e.g.
+ * {@link RecyclerViewParallax} calls {@link #updateValues()} in RecyclerView scrolling. App might
+ * call {@link #updateValues()} manually when Parallax is unaware of the value change. For example,
+ * when a slide transition is running, {@link RecyclerViewParallax} is unaware of translation value
+ * changes; it's the app's responsibility to call {@link #updateValues()} in every frame of
+ * animation.
+ * </p>
+ * @param <PropertyT> Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}.
  */
-public final class Parallax {
+public abstract class Parallax<PropertyT extends Property> {
 
-    private ParallaxSource mSource;
     private final List<ParallaxEffect> mEffects = new ArrayList<ParallaxEffect>(4);
 
-    private final ParallaxSource.Listener mSourceListener = new ParallaxSource.Listener() {
+    /**
+     * Class holding a fixed value for a Property in {@link Parallax}.
+     * Base class for {@link IntPropertyMarkerValue} and {@link FloatPropertyMarkerValue}.
+     * @param <PropertyT> Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}.
+     */
+    public static class PropertyMarkerValue<PropertyT> {
+        private final PropertyT mProperty;
+
+        public PropertyMarkerValue(PropertyT property) {
+            mProperty = property;
+        }
+
+        /**
+         * @return Associated property.
+         */
+        public PropertyT getProperty() {
+            return mProperty;
+        }
+    }
+
+    /**
+     * IntProperty provide access to an index based integer type property inside
+     * {@link IntParallax}. The IntProperty typically represents UI element position inside
+     * {@link IntParallax}.
+     */
+    public static class IntProperty extends Property<IntParallax, Integer> {
+
+        /**
+         * Property value is unknown and it's smaller than minimal value of Parallax. For
+         * example if a child is not created and before the first visible child of RecyclerView.
+         */
+        public static final int UNKNOWN_BEFORE = Integer.MIN_VALUE;
+
+        /**
+         * Property value is unknown and it's larger than {@link IntParallax#getMaxValue()}. For
+         * example if a child is not created and after the last visible child of RecyclerView.
+         */
+        public static final int UNKNOWN_AFTER = Integer.MAX_VALUE;
+
+        private final int mIndex;
+
+        /**
+         * Constructor.
+         *
+         * @param name Name of this Property.
+         * @param index Index of this Property inside {@link IntParallax}.
+         */
+        public IntProperty(String name, int index) {
+            super(Integer.class, name);
+            mIndex = index;
+        }
+
         @Override
-        public void onPropertiesChanged(ParallaxSource source) {
-            for (int i = 0; i < mEffects.size(); i++) {
-                mEffects.get(i).performMapping(source);
+        public final Integer get(IntParallax object) {
+            return getIntValue(object);
+        }
+
+        @Override
+        public final void set(IntParallax object, Integer value) {
+            setIntValue(object, value);
+        }
+
+        final int getIntValue(IntParallax source) {
+            return source.getPropertyValue(mIndex);
+        }
+
+        final void setIntValue(IntParallax source, int value) {
+            source.setPropertyValue(mIndex, value);
+        }
+
+        /**
+         * @return Index of this Property in {@link IntParallax}.
+         */
+        public final int getIndex() {
+            return mIndex;
+        }
+
+        /**
+         * Creates an {@link IntPropertyMarkerValue} object for the absolute marker value.
+         *
+         * @param absoluteValue The integer marker value.
+         * @return A new {@link IntPropertyMarkerValue} object.
+         */
+        public final IntPropertyMarkerValue atAbsolute(int absoluteValue) {
+            return new IntPropertyMarkerValue(this, absoluteValue, 0f);
+        }
+
+        /**
+         * Creates an {@link IntPropertyMarkerValue} object for a fraction of
+         * {@link IntParallax#getMaxValue()}.
+         *
+         * @param fractionOfMaxValue 0 to 1 fraction to multiply with
+         *                                       {@link IntParallax#getMaxValue()} for
+         *                                       the marker value.
+         * @return A new {@link IntPropertyMarkerValue} object.
+         */
+        public final IntPropertyMarkerValue atFraction(float fractionOfMaxValue) {
+            return new IntPropertyMarkerValue(this, 0, fractionOfMaxValue);
+        }
+
+        /**
+         * Create an {@link IntPropertyMarkerValue} object by multiplying the fraction with
+         * {@link IntParallax#getMaxValue()} and adding offsetValue to it.
+         *
+         * @param offsetValue                    An offset integer value to be added to marker
+         *                                       value.
+         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
+         *                                       {@link IntParallax#getMaxValue()} for
+         *                                       the marker value.
+         * @return A new {@link IntPropertyMarkerValue} object.
+         */
+        public final IntPropertyMarkerValue at(int offsetValue,
+                                               float fractionOfMaxParentVisibleSize) {
+            return new IntPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize);
+        }
+    }
+
+    /**
+     * Parallax that manages a list of {@link IntProperty}. App may override this class with a
+     * specific {@link IntProperty} subclass.
+     *
+     * @param <IntPropertyT> Type of {@link IntProperty} or subclass.
+     */
+    public abstract static class IntParallax<IntPropertyT extends IntProperty>
+            extends Parallax<IntPropertyT> {
+
+        private int[] mValues = new int[4];
+
+        /**
+         * Get index based property value.
+         *
+         * @param index Index of the property.
+         * @return Value of the property.
+         */
+        public final int getPropertyValue(int index) {
+            return mValues[index];
+        }
+
+        /**
+         * Set index based property value.
+         *
+         * @param index Index of the property.
+         * @param value Value of the property.
+         */
+        public final void setPropertyValue(int index, int value) {
+            if (index >= mProperties.size()) {
+                throw new ArrayIndexOutOfBoundsException();
+            }
+            mValues[index] = value;
+        }
+
+        /**
+         * Return the max value, which is typically parent visible area, e.g. RecyclerView's height
+         * if we are tracking Y position of a child. The size can be used to calculate marker value
+         * using the provided fraction of IntPropertyMarkerValue.
+         *
+         * @return Max value of parallax.
+         * @see IntPropertyMarkerValue#IntPropertyMarkerValue(IntProperty, int, float)
+         */
+        public abstract int getMaxValue();
+
+        @Override
+        public final IntPropertyT addProperty(String name) {
+            int newPropertyIndex = mProperties.size();
+            IntPropertyT property = createProperty(name, newPropertyIndex);
+            mProperties.add(property);
+            int size = mValues.length;
+            if (size == newPropertyIndex) {
+                int[] newValues = new int[size * 2];
+                for (int i = 0; i < size; i++) {
+                    newValues[i] = mValues[i];
+                }
+                mValues = newValues;
+            }
+            mValues[newPropertyIndex] = IntProperty.UNKNOWN_AFTER;
+            return property;
+        }
+
+        @Override
+        public final void verifyProperties() throws IllegalStateException {
+            if (mProperties.size() < 2) {
+                return;
+            }
+            int last = mProperties.get(0).getIntValue(this);
+            for (int i = 1; i < mProperties.size(); i++) {
+                int v = mProperties.get(i).getIntValue(this);
+                if (v < last) {
+                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+                                    + " smaller than Property[%d]\"%s\"",
+                            i, mProperties.get(i).getName(),
+                            i - 1, mProperties.get(i - 1).getName()));
+                } else if (last == IntProperty.UNKNOWN_BEFORE && v == IntProperty.UNKNOWN_AFTER) {
+                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+                                    + " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
+                            i - 1, mProperties.get(i - 1).getName(),
+                            i, mProperties.get(i).getName()));
+                }
+                last = v;
             }
         }
-    };
+
+    }
 
     /**
-     * Sets a {@link ParallaxSource} object and starts listening on it. Stops listening to the
-     * previous {@link ParallaxSource} object if it exists.
-     *
-     * @param source New {@link ParallaxSource} object.
+     * Implementation of {@link PropertyMarkerValue} for {@link IntProperty}.
      */
-    public void setSource(ParallaxSource source) {
-        if (mSource != null) {
-            mSource.setListener(null);
+    public static class IntPropertyMarkerValue extends PropertyMarkerValue<IntProperty> {
+        private final int mValue;
+        private final float mFactionOfMax;
+
+        public IntPropertyMarkerValue(IntProperty property, int value) {
+            this(property, value, 0f);
         }
-        mSource = source;
-        if (mSource != null) {
-            mSource.setListener(mSourceListener);
+
+        public IntPropertyMarkerValue(IntProperty property, int value, float fractionOfMax) {
+            super(property);
+            mValue = value;
+            mFactionOfMax = fractionOfMax;
+        }
+
+        /**
+         * @return The marker value of integer type.
+         */
+        public final int getMarkerValue(IntParallax source) {
+            return mFactionOfMax == 0 ? mValue : mValue + Math.round(source
+                    .getMaxValue() * mFactionOfMax);
         }
     }
 
     /**
-     * Gets the current {@link ParallaxSource} object.
-     *
-     * @return The current {@link ParallaxSource} Object.
+     * FloatProperty provide access to an index based integer type property inside
+     * {@link FloatParallax}. The FloatProperty typically represents UI element position inside
+     * {@link FloatParallax}.
      */
-    public ParallaxSource getSource() {
-        return mSource;
+    public static class FloatProperty extends Property<FloatParallax, Float> {
+
+        /**
+         * Property value is unknown and it's smaller than minimal value of Parallax. For
+         * example if a child is not created and before the first visible child of RecyclerView.
+         */
+        public static final float UNKNOWN_BEFORE = -Float.MAX_VALUE;
+
+        /**
+         * Property value is unknown and it's larger than {@link FloatParallax#getMaxValue()}. For
+         * example if a child is not created and after the last visible child of RecyclerView.
+         */
+        public static final float UNKNOWN_AFTER = Float.MAX_VALUE;
+
+        private final int mIndex;
+
+        /**
+         * Constructor.
+         *
+         * @param name Name of this Property.
+         * @param index Index of this Property inside {@link FloatParallax}.
+         */
+        public FloatProperty(String name, int index) {
+            super(Float.class, name);
+            mIndex = index;
+        }
+
+        @Override
+        public final Float get(FloatParallax object) {
+            return getFloatValue(object);
+        }
+
+        @Override
+        public final void set(FloatParallax object, Float value) {
+            setFloatValue(object, value);
+        }
+
+        final float getFloatValue(FloatParallax source) {
+            return source.getPropertyValue(mIndex);
+        }
+
+        final void setFloatValue(FloatParallax source, float value) {
+            source.setPropertyValue(mIndex, value);
+        }
+
+        /**
+         * @return Index of this Property in {@link FloatParallax}.
+         */
+        public final int getIndex() {
+            return mIndex;
+        }
+
+        /**
+         * Creates an {@link FloatPropertyMarkerValue} object for the absolute marker value.
+         *
+         * @param markerValue The float marker value.
+         * @return A new {@link FloatPropertyMarkerValue} object.
+         */
+        public final FloatPropertyMarkerValue atAbsolute(float markerValue) {
+            return new FloatPropertyMarkerValue(this, markerValue, 0f);
+        }
+
+        /**
+         * Creates an {@link FloatPropertyMarkerValue} object for a fraction of
+         * {@link FloatParallax#getMaxValue()}.
+         *
+         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
+         *                                       {@link FloatParallax#getMaxValue()} for
+         *                                       the marker value.
+         * @return A new {@link FloatPropertyMarkerValue} object.
+         */
+        public final FloatPropertyMarkerValue atFraction(float fractionOfMaxParentVisibleSize) {
+            return new FloatPropertyMarkerValue(this, 0, fractionOfMaxParentVisibleSize);
+        }
+
+        /**
+         * Create an {@link FloatPropertyMarkerValue} object by multiplying the fraction with
+         * {@link FloatParallax#getMaxValue()} and adding offsetValue to it.
+         *
+         * @param offsetValue                    An offset float value to be added to marker value.
+         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
+         *                                       {@link FloatParallax#getMaxValue()} for
+         *                                       the marker value.
+         * @return A new {@link FloatPropertyMarkerValue} object.
+         */
+        public final FloatPropertyMarkerValue at(float offsetValue,
+                                                 float fractionOfMaxParentVisibleSize) {
+            return new FloatPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize);
+        }
     }
 
     /**
-     * Adds a {@link ParallaxEffect} object which defines rules to perform mapping from
-     * {@link ParallaxSource} to multiple {@link ParallaxTarget}s.
+     * Parallax that manages a list of {@link FloatProperty}. App may override this class with a
+     * specific {@link FloatProperty} subclass.
+     *
+     * @param <FloatPropertyT> Type of {@link FloatProperty} or subclass.
+     */
+    public abstract static class FloatParallax<FloatPropertyT extends FloatProperty> extends
+            Parallax<FloatPropertyT> {
+
+        private float[] mValues = new float[4];
+
+        /**
+         * Get index based property value.
+         *
+         * @param index Index of the property.
+         * @return Value of the property.
+         */
+        public final float getPropertyValue(int index) {
+            return mValues[index];
+        }
+
+        /**
+         * Set index based property value.
+         *
+         * @param index Index of the property.
+         * @param value Value of the property.
+         */
+        public final void setPropertyValue(int index, float value) {
+            if (index >= mProperties.size()) {
+                throw new ArrayIndexOutOfBoundsException();
+            }
+            mValues[index] = value;
+        }
+
+        /**
+         * Return the max value which is typically size of parent visible area, e.g. RecyclerView's
+         * height if we are tracking Y position of a child. The size can be used to calculate marker
+         * value using the provided fraction of FloatPropertyMarkerValue.
+         *
+         * @return Size of parent visible area.
+         * @see FloatPropertyMarkerValue#FloatPropertyMarkerValue(FloatProperty, float, float)
+         */
+        public abstract float getMaxValue();
+
+        @Override
+        public final FloatPropertyT addProperty(String name) {
+            int newPropertyIndex = mProperties.size();
+            FloatPropertyT property = createProperty(name, newPropertyIndex);
+            mProperties.add(property);
+            int size = mValues.length;
+            if (size == newPropertyIndex) {
+                float[] newValues = new float[size * 2];
+                for (int i = 0; i < size; i++) {
+                    newValues[i] = mValues[i];
+                }
+                mValues = newValues;
+            }
+            mValues[newPropertyIndex] = FloatProperty.UNKNOWN_AFTER;
+            return property;
+        }
+
+        @Override
+        public final void verifyProperties() throws IllegalStateException {
+            if (mProperties.size() < 2) {
+                return;
+            }
+            float last = mProperties.get(0).getFloatValue(this);
+            for (int i = 1; i < mProperties.size(); i++) {
+                float v = mProperties.get(i).getFloatValue(this);
+                if (v < last) {
+                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+                                    + " smaller than Property[%d]\"%s\"",
+                            i, mProperties.get(i).getName(),
+                            i - 1, mProperties.get(i - 1).getName()));
+                } else if (last == FloatProperty.UNKNOWN_BEFORE && v
+                        == FloatProperty.UNKNOWN_AFTER) {
+                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+                                    + " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
+                            i - 1, mProperties.get(i - 1).getName(),
+                            i, mProperties.get(i).getName()));
+                }
+                last = v;
+            }
+        }
+
+    }
+
+    /**
+     * Implementation of {@link PropertyMarkerValue} for {@link FloatProperty}.
+     */
+    public static class FloatPropertyMarkerValue extends PropertyMarkerValue<FloatProperty> {
+        private final float mValue;
+        private final float mFactionOfMax;
+
+        public FloatPropertyMarkerValue(FloatProperty property, float value) {
+            this(property, value, 0f);
+        }
+
+        public FloatPropertyMarkerValue(FloatProperty property, float value, float fractionOfMax) {
+            super(property);
+            mValue = value;
+            mFactionOfMax = fractionOfMax;
+        }
+
+        /**
+         * @return The marker value.
+         */
+        public final float getMarkerValue(FloatParallax source) {
+            return mFactionOfMax == 0 ? mValue : mValue + source.getMaxValue()
+                    * mFactionOfMax;
+        }
+    }
+
+    final List<PropertyT> mProperties = new ArrayList<PropertyT>();
+    final List<PropertyT> mPropertiesReadOnly = Collections.unmodifiableList(mProperties);
+
+    /**
+     * @return A unmodifiable list of properties.
+     */
+    public final List<PropertyT> getProperties() {
+        return mPropertiesReadOnly;
+    }
+
+    /**
+     * Add a new Property in the Parallax object.
+     *
+     * @param name Name of the property.
+     * @return Newly created Property.
+     */
+    public abstract PropertyT addProperty(String name);
+
+    /**
+     * Create a new Property object. App does not directly call this method.  See
+     * {@link #addProperty(String)}.
+     *
+     * @param index  Index of the property in this Parallax object.
+     * @return Newly created Property object.
+     */
+    public abstract PropertyT createProperty(String name, int index);
+
+    /**
+     * Verify sanity of property values, throws RuntimeException if fails. The property values
+     * must be in ascending order. UNKNOW_BEFORE and UNKNOWN_AFTER are not allowed to be next to
+     * each other.
+     */
+    public abstract void verifyProperties() throws IllegalStateException;
+
+    /**
+     * Update property values and perform {@link ParallaxEffect}s. Subclass may override and call
+     * super.updateValues() after updated properties values.
+     */
+    @CallSuper
+    public void updateValues() {
+        for (int i = 0; i < mEffects.size(); i++) {
+            mEffects.get(i).performMapping(this);
+        }
+    }
+
+    /**
+     * Adds a {@link ParallaxEffect} object which defines rules to perform mapping to multiple
+     * {@link ParallaxTarget}s.
      *
      * @param effect A {@link ParallaxEffect} object.
      */
@@ -76,8 +554,8 @@
     }
 
     /**
-     * Returns a list of {@link ParallaxEffect} object which defines rules to perform mapping from
-     * {@link ParallaxSource} to multiple {@link ParallaxTarget}s.
+     * Returns a list of {@link ParallaxEffect} object which defines rules to perform mapping to
+     * multiple {@link ParallaxTarget}s.
      *
      * @return A list of {@link ParallaxEffect} object.
      */
@@ -105,10 +583,10 @@
      * Create a {@link ParallaxEffect} object that will track source variable changes within a
      * provided set of ranges.
      *
-     * @param ranges A list of key values that defines the ranges.
+     * @param ranges A list of marker values that defines the ranges.
      * @return Newly created ParallaxEffect object.
      */
-    public ParallaxEffect addEffect(IntPropertyKeyValue... ranges) {
+    public ParallaxEffect addEffect(IntPropertyMarkerValue... ranges) {
         IntEffect effect = new IntEffect();
         effect.setPropertyRanges(ranges);
         addEffect(effect);
@@ -119,10 +597,10 @@
      * Create a {@link ParallaxEffect} object that will track source variable changes within a
      * provided set of ranges.
      *
-     * @param ranges A list of key values that defines the ranges.
+     * @param ranges A list of marker values that defines the ranges.
      * @return Newly created ParallaxEffect object.
      */
-    public ParallaxEffect addEffect(FloatPropertyKeyValue... ranges) {
+    public ParallaxEffect addEffect(FloatPropertyMarkerValue... ranges) {
         FloatEffect effect = new FloatEffect();
         effect.setPropertyRanges(ranges);
         addEffect(effect);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java b/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
index 28731fd..5760bdb 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
@@ -1,64 +1,65 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
+ * 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
+ *      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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
+
 package android.support.v17.leanback.widget;
 
 import android.animation.PropertyValuesHolder;
-import android.support.v17.leanback.widget.ParallaxSource.FloatProperty;
-import android.support.v17.leanback.widget.ParallaxSource.FloatPropertyKeyValue;
-import android.support.v17.leanback.widget.ParallaxSource.IntProperty;
-import android.support.v17.leanback.widget.ParallaxSource.IntPropertyKeyValue;
-import android.support.v17.leanback.widget.ParallaxSource.PropertyKeyValue;
+import android.support.v17.leanback.widget.Parallax.FloatProperty;
+import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue;
+import android.support.v17.leanback.widget.Parallax.IntProperty;
+import android.support.v17.leanback.widget.Parallax.PropertyMarkerValue;
 
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  * ParallaxEffect class drives changes in {@link ParallaxTarget} in response to changes in
- * variables defined in {@link ParallaxSource}.
+ * variables defined in {@link Parallax}.
  * <p>
- * ParallaxEffect has a list of {@link PropertyKeyValue}s which represents the range of values that
- * source variables can take. The main function is
- * {@link ParallaxEffect#performMapping(ParallaxSource)} which computes a fraction between 0 and 1
- * based on the current values of variables in {@link ParallaxSource}. As the parallax effect goes
+ * ParallaxEffect has a list of {@link Parallax.PropertyMarkerValue}s which represents the range of
+ * values that source variables can take. The main function is
+ * {@link ParallaxEffect#performMapping(Parallax)} which computes a fraction between 0 and 1
+ * based on the current values of variables in {@link Parallax}. As the parallax effect goes
  * on, the fraction increases from 0 at beginning to 1 at the end. Then the fraction is passed on
  * to {@link ParallaxTarget#update(float)}.
  * <p>
  * ParallaxEffect has two concrete subclasses, {@link IntEffect} and {@link FloatEffect}.
- * @hide
  */
 public abstract class ParallaxEffect<ParallaxEffectT extends ParallaxEffect,
-        PropertyKeyValueT extends ParallaxSource.PropertyKeyValue> {
+        PropertyMarkerValueT extends Parallax.PropertyMarkerValue> {
 
-    final List<PropertyKeyValueT> mKeyValues = new ArrayList<PropertyKeyValueT>(2);
+    final List<PropertyMarkerValueT> mMarkerValues = new ArrayList<PropertyMarkerValueT>(2);
     final List<Float> mWeights = new ArrayList<Float>(2);
     final List<Float> mTotalWeights = new ArrayList<Float>(2);
     final List<ParallaxTarget> mTargets = new ArrayList<ParallaxTarget>(4);
 
     /**
-     * Returns the list of {@link PropertyKeyValue}s, which represents the range of values that
+     * Returns the list of {@link PropertyMarkerValue}s, which represents the range of values that
      * source variables can take.
      *
-     * @return A list of {@link PropertyKeyValue}s.
-     * @see #performMapping(ParallaxSource)
+     * @return A list of {@link Parallax.PropertyMarkerValue}s.
+     * @see #performMapping(Parallax)
      */
-    public final List<PropertyKeyValueT> getPropertyRanges() {
-        return mKeyValues;
+    public final List<PropertyMarkerValueT> getPropertyRanges() {
+        return  mMarkerValues;
     }
 
     /**
      * Returns a list of Float objects that represents weight associated with each variable range.
-     * Weights are used when there are three or more key values.
+     * Weights are used when there are three or more marker values.
      *
      * @return A list of Float objects that represents weight associated with each variable range.
      * @hide
@@ -68,22 +69,22 @@
     }
 
     /**
-     * Sets the list of {@link PropertyKeyValue}s, which represents the range of values that
+     * Sets the list of {@link PropertyMarkerValue}s, which represents the range of values that
      * source variables can take.
      *
-     * @param keyValues A list of {@link PropertyKeyValue}s.
-     * @see #performMapping(ParallaxSource)
+     * @param markerValues A list of {@link PropertyMarkerValue}s.
+     * @see #performMapping(Parallax)
      */
-    public final void setPropertyRanges(PropertyKeyValueT... keyValues) {
-        mKeyValues.clear();
-        for (PropertyKeyValueT keyValue : keyValues) {
-            mKeyValues.add(keyValue);
+    public final void setPropertyRanges(PropertyMarkerValueT... markerValues) {
+        mMarkerValues.clear();
+        for (PropertyMarkerValueT markerValue : markerValues) {
+            mMarkerValues.add(markerValue);
         }
     }
 
     /**
      * Sets a list of Float objects that represents weight associated with each variable range.
-     * Weights are used when there are three or more key values.
+     * Weights are used when there are three or more marker values.
      *
      * @param weights A list of Float objects that represents weight associated with each variable
      *                range.
@@ -107,7 +108,7 @@
 
     /**
      * Sets a list of Float objects that represents weight associated with each variable range.
-     * Weights are used when there are three or more key values.
+     * Weights are used when there are three or more marker values.
      *
      * @param weights A list of Float objects that represents weight associated with each variable
      *                range.
@@ -170,10 +171,10 @@
     }
 
     /**
-     * Perform mapping from {@link ParallaxSource} to list of {@link ParallaxTarget}.
+     * Perform mapping from {@link Parallax} to list of {@link ParallaxTarget}.
      */
-    public final void performMapping(ParallaxSource source) {
-        if (mKeyValues.size() < 2) {
+    public final void performMapping(Parallax source) {
+        if (mMarkerValues.size() < 2) {
             return;
         }
         source.verifyProperties();
@@ -185,37 +186,37 @@
 
     /**
      * This method is expected to compute a fraction between 0 and 1 based on the current values of
-     * variables in {@link ParallaxSource}. As the parallax effect goes on, the fraction increases
+     * variables in {@link Parallax}. As the parallax effect goes on, the fraction increases
      * from 0 at beginning to 1 at the end.
      *
      * @return Float value between 0 and 1.
      */
-    protected abstract float calculateFraction(ParallaxSource source);
+    protected abstract float calculateFraction(Parallax source);
 
     /**
-     * When there are multiple ranges (aka three or more keyvalues),  this method adjust the
+     * When there are multiple ranges (aka three or more markerValues),  this method adjust the
      * fraction inside a range to fraction of whole range.
-     * e.g. four key values, three weight values: 6, 2, 2.  totalWeights are 6, 8, 10
-     * When keyValueIndex is 3, the fraction is inside last range.
+     * e.g. four marker values, three weight values: 6, 2, 2.  totalWeights are 6, 8, 10
+     * When markerValueIndex is 3, the fraction is inside last range.
      * adjusted_fraction = 8 / 10 + 2 / 10 * fraction.
      */
-    final float getFractionWithWeightAdjusted(float fraction, int keyValueIndex) {
-        // when there are three or more KeyValues, take weight into consideration.
-        if (mKeyValues.size() >= 3) {
-            final boolean hasWeightsDefined = mWeights.size() == mKeyValues.size() - 1;
+    final float getFractionWithWeightAdjusted(float fraction, int markerValueIndex) {
+        // when there are three or more markerValues, take weight into consideration.
+        if (mMarkerValues.size() >= 3) {
+            final boolean hasWeightsDefined = mWeights.size() == mMarkerValues.size() - 1;
             if (hasWeightsDefined) {
                 // use weights user defined
                 final float allWeights = mTotalWeights.get(mTotalWeights.size() - 1);
-                fraction = fraction * mWeights.get(keyValueIndex - 1) / allWeights;
-                if (keyValueIndex >= 2) {
-                    fraction += mTotalWeights.get(keyValueIndex - 2) / allWeights;
+                fraction = fraction * mWeights.get(markerValueIndex - 1) / allWeights;
+                if (markerValueIndex >= 2) {
+                    fraction += mTotalWeights.get(markerValueIndex - 2) / allWeights;
                 }
             } else {
                 // assume each range has same weight.
-                final float allWeights = mKeyValues.size() - 1;
+                final float allWeights =  mMarkerValues.size() - 1;
                 fraction = fraction / allWeights;
-                if (keyValueIndex >= 2) {
-                    fraction += (float)(keyValueIndex - 1) / allWeights;
+                if (markerValueIndex >= 2) {
+                    fraction += (float) (markerValueIndex - 1) / allWeights;
                 }
             }
         }
@@ -225,65 +226,66 @@
     /**
      * Implementation of {@link ParallaxEffect} for integer type.
      */
-    public static final class IntEffect extends ParallaxEffect<IntEffect, IntPropertyKeyValue> {
+    public static final class IntEffect extends ParallaxEffect<IntEffect,
+            Parallax.IntPropertyMarkerValue> {
 
         @Override
-        protected float calculateFraction(ParallaxSource s) {
-            ParallaxSource.IntSource source = (ParallaxSource.IntSource) s;
+        protected float calculateFraction(Parallax s) {
+            Parallax.IntParallax source = (Parallax.IntParallax) s;
             int lastIndex = 0;
             int lastValue = 0;
-            int lastKeyValue = 0;
-            // go through all KeyValues, find first KeyValue that current value is less than.
-            for (int i = 0; i < mKeyValues.size(); i++) {
-                IntPropertyKeyValue k = mKeyValues.get(i);
+            int lastMarkerValue = 0;
+            // go through all markerValues, find first markerValue that current value is less than.
+            for (int i = 0; i <  mMarkerValues.size(); i++) {
+                Parallax.IntPropertyMarkerValue k =  mMarkerValues.get(i);
                 int index = k.getProperty().getIndex();
-                int keyValue = k.getKeyValue(source);
+                int markerValue = k.getMarkerValue(source);
                 int currentValue = source.getPropertyValue(index);
 
                 float fraction;
                 if (i == 0) {
-                    if (currentValue >= keyValue) {
+                    if (currentValue >= markerValue) {
                         return 0f;
                     }
                 } else {
-                    if (lastIndex == index && lastKeyValue < keyValue) {
-                        throw new IllegalStateException("KeyValue of same variable must be "
+                    if (lastIndex == index && lastMarkerValue < markerValue) {
+                        throw new IllegalStateException("marker value of same variable must be "
                                 + "descendant order");
                     }
                     if (currentValue == IntProperty.UNKNOWN_AFTER) {
-                        // Implies lastValue is less than lastKeyValue and lastValue is not
+                        // Implies lastValue is less than lastMarkerValue and lastValue is not
                         // UNKNWON_AFTER.  Estimates based on distance of two variables is screen
                         // size.
-                        fraction = (float) (lastKeyValue - lastValue)
-                                / source.getMaxParentVisibleSize();
+                        fraction = (float) (lastMarkerValue - lastValue)
+                                / source.getMaxValue();
                         return getFractionWithWeightAdjusted(fraction, i);
-                    } else if (currentValue >= keyValue) {
+                    } else if (currentValue >= markerValue) {
                         if (lastIndex == index) {
-                            // same variable index,  same UI element at two different KeyValues,
-                            // e.g. UI element moves from lastkeyValue=500 to keyValue=0,
+                            // same variable index,  same UI element at two different MarkerValues,
+                            // e.g. UI element moves from lastMarkerValue=500 to markerValue=0,
                             // fraction moves from 0 to 1.
-                            fraction = (float) (lastKeyValue - currentValue)
-                                    / (lastKeyValue - keyValue);
+                            fraction = (float) (lastMarkerValue - currentValue)
+                                    / (lastMarkerValue - markerValue);
                         } else if (lastValue != IntProperty.UNKNOWN_BEFORE) {
                             // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when
-                            // UIElement_1 is at keyValue=300,  keyValue of UIElement_2 by adding
-                            // delta of values to keyValue of UIElement_2.
-                            lastKeyValue = lastKeyValue + (currentValue - lastValue);
-                            fraction = (float) (lastKeyValue - currentValue)
-                                    / (lastKeyValue - keyValue);
+                            // UIElement_1 is at markerValue=300,  markerValue of UIElement_2 by
+                            // adding delta of values to markerValue of UIElement_2.
+                            lastMarkerValue = lastMarkerValue + (currentValue - lastValue);
+                            fraction = (float) (lastMarkerValue - currentValue)
+                                    / (lastMarkerValue - markerValue);
                         } else {
                             // Last variable is UNKNOWN_BEFORE.  Estimates based on assumption total
                             // travel distance from last variable to this variable is screen visible
                             // size.
-                            fraction = 1f - (float) (currentValue - keyValue)
-                                    / source.getMaxParentVisibleSize();
+                            fraction = 1f - (float) (currentValue - markerValue)
+                                    / source.getMaxValue();
                         }
                         return getFractionWithWeightAdjusted(fraction, i);
                     }
                 }
                 lastValue = currentValue;
                 lastIndex = index;
-                lastKeyValue = keyValue;
+                lastMarkerValue = markerValue;
             }
             return 1f;
         }
@@ -293,65 +295,65 @@
      * Implementation of {@link ParallaxEffect} for float type.
      */
     public static final class FloatEffect extends ParallaxEffect<FloatEffect,
-            FloatPropertyKeyValue> {
+            Parallax.FloatPropertyMarkerValue> {
 
         @Override
-        protected float calculateFraction(ParallaxSource s) {
-            ParallaxSource.FloatSource source = (ParallaxSource.FloatSource) s;
+        protected float calculateFraction(Parallax s) {
+            Parallax.FloatParallax source = (Parallax.FloatParallax) s;
             int lastIndex = 0;
             float lastValue = 0;
-            float lastKeyValue = 0;
-            // go through all KeyValues, find first KeyValue that current value is less than.
-            for (int i = 0; i < mKeyValues.size(); i++) {
-                FloatPropertyKeyValue k = mKeyValues.get(i);
+            float lastMarkerValue = 0;
+            // go through all markerValues, find first markerValue that current value is less than.
+            for (int i = 0; i <  mMarkerValues.size(); i++) {
+                FloatPropertyMarkerValue k =  mMarkerValues.get(i);
                 int index = k.getProperty().getIndex();
-                float keyValue = k.getKeyValue(source);
+                float markerValue = k.getMarkerValue(source);
                 float currentValue = source.getPropertyValue(index);
 
                 float fraction;
                 if (i == 0) {
-                    if (currentValue >= keyValue) {
+                    if (currentValue >= markerValue) {
                         return 0f;
                     }
                 } else {
-                    if (lastIndex == index && lastKeyValue < keyValue) {
-                        throw new IllegalStateException("KeyValue of same variable must be "
+                    if (lastIndex == index && lastMarkerValue < markerValue) {
+                        throw new IllegalStateException("marker value of same variable must be "
                                 + "descendant order");
                     }
                     if (currentValue == FloatProperty.UNKNOWN_AFTER) {
-                        // Implies lastValue is less than lastKeyValue and lastValue is not
+                        // Implies lastValue is less than lastMarkerValue and lastValue is not
                         // UNKNOWN_AFTER.  Estimates based on distance of two variables is screen
                         // size.
-                        fraction = (float) (lastKeyValue - lastValue)
-                                / source.getMaxParentVisibleSize();
+                        fraction = (float) (lastMarkerValue - lastValue)
+                                / source.getMaxValue();
                         return getFractionWithWeightAdjusted(fraction, i);
-                    } else if (currentValue >= keyValue) {
+                    } else if (currentValue >= markerValue) {
                         if (lastIndex == index) {
-                            // same variable index,  same UI element at two different KeyValues,
-                            // e.g. UI element moves from lastkeyValue=500 to keyValue=0,
+                            // same variable index,  same UI element at two different MarkerValues,
+                            // e.g. UI element moves from lastMarkerValue=500 to markerValue=0,
                             // fraction moves from 0 to 1.
-                            fraction = (float) (lastKeyValue - currentValue)
-                                    / (lastKeyValue - keyValue);
+                            fraction = (float) (lastMarkerValue - currentValue)
+                                    / (lastMarkerValue - markerValue);
                         } else if (lastValue != FloatProperty.UNKNOWN_BEFORE) {
                             // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when
-                            // UIElement_1 is at keyValue=300,  keyValue of UIElement_2 by adding
-                            // delta of values to keyValue of UIElement_2.
-                            lastKeyValue = lastKeyValue + (currentValue - lastValue);
-                            fraction = (float) (lastKeyValue - currentValue)
-                                    / (lastKeyValue - keyValue);
+                            // UIElement_1 is at markerValue=300,  markerValue of UIElement_2 by
+                            // adding delta of values to markerValue of UIElement_2.
+                            lastMarkerValue = lastMarkerValue + (currentValue - lastValue);
+                            fraction = (float) (lastMarkerValue - currentValue)
+                                    / (lastMarkerValue - markerValue);
                         } else {
                             // Last variable is UNKNOWN_BEFORE.  Estimates based on assumption total
                             // travel distance from last variable to this variable is screen visible
                             // size.
-                            fraction = 1f - (float) (currentValue - keyValue)
-                                    / source.getMaxParentVisibleSize();
+                            fraction = 1f - (float) (currentValue - markerValue)
+                                    / source.getMaxValue();
                         }
                         return getFractionWithWeightAdjusted(fraction, i);
                     }
                 }
                 lastValue = currentValue;
                 lastIndex = index;
-                lastKeyValue = keyValue;
+                lastMarkerValue = markerValue;
             }
             return 1f;
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxSource.java b/v17/leanback/src/android/support/v17/leanback/widget/ParallaxSource.java
deleted file mode 100644
index c8ba362..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxSource.java
+++ /dev/null
@@ -1,525 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import android.util.Property;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-
-/**
- * ParallaxSource tracks a list of dynamic {@link Property}s typically representing foreground UI
- * element positions on screen. App should subclass either {@link ParallaxSource.IntSource} or
- * {@link ParallaxSource.FloatSource}. App may subclass {@link ParallaxSource.IntProperty} or
- * {@link ParallaxSource.FloatProperty} to supply additional information about how to retrieve
- * Property value.  For reference implementation, see {@link ParallaxRecyclerViewSource}.
- *
- * <p>
- * <ul>Restrictions
- * <li>Values must be in ascending order.</li>
- * <li>If the UI element is unknown above screen, use UNKNOWN_BEFORE.</li>
- * <li>if the UI element is unknown below screen, use UNKNOWN_AFTER.</li>
- * <li>UNKNOWN_BEFORE and USE_UNKNOWN_AFTER are not allowed to be next to each other.</li>
- * </ul>
- *
- * These rules can be verified by {@link #verifyProperties()}.
- * </p>
- * @hide
- */
-public abstract class ParallaxSource<PropertyT extends Property> {
-
-    /**
-     * Listener for tracking Property value changes.
-     */
-    public static abstract class Listener {
-        /**
-         * Called when the value for any of the property in ParallaxSource changes.
-         */
-        public void onPropertiesChanged(ParallaxSource source) {
-        }
-    }
-
-    /**
-     * Class holding a fixed key value for a Property in {@link ParallaxSource}.
-     * Base class for {@link IntPropertyKeyValue} and {@link FloatPropertyKeyValue}.
-     */
-    public static class PropertyKeyValue<PropertyT extends Property> {
-        private final PropertyT mProperty;
-
-        public PropertyKeyValue(PropertyT property) {
-            mProperty = property;
-        }
-
-        /**
-         * @return Associated property.
-         */
-        public PropertyT getProperty() {
-            return mProperty;
-        }
-    }
-
-    /**
-     * IntProperty provide access to an index based integer type property inside {@link IntSource}.
-     * The IntProperty typically represents UI element position inside {@link IntSource}.
-     */
-    public static class IntProperty extends Property<IntSource, Integer> {
-
-        /**
-         * Property value is unknown and it's above screen.
-         */
-        public static final int UNKNOWN_BEFORE = Integer.MIN_VALUE;
-
-        /**
-         * Property value is unknown and it's bellow screen.
-         */
-        public static final int UNKNOWN_AFTER = Integer.MAX_VALUE;
-
-        private final int mIndex;
-
-        /**
-         * Constructor.
-         *
-         * @param name Name of this Property.
-         * @param index Index of this Property inside {@link IntSource}.
-         */
-        public IntProperty(String name, int index) {
-            super(Integer.class, name);
-            mIndex = index;
-        }
-
-        @Override
-        public final Integer get(IntSource object) {
-            return getIntValue(object);
-        }
-
-        @Override
-        public final void set(IntSource object, Integer value) {
-            setIntValue(object, value);
-        }
-
-        final int getIntValue(IntSource source) {
-            return source.getPropertyValue(mIndex);
-        }
-
-        final void setIntValue(IntSource source, int value) {
-            source.setPropertyValue(mIndex, value);
-        }
-
-        /**
-         * @return Index of this Property in {@link IntSource}.
-         */
-        public final int getIndex() {
-            return mIndex;
-        }
-
-        /**
-         * Creates an {@link IntPropertyKeyValue} object for the absolute keyValue.
-         *
-         * @param keyValue The integer key value.
-         * @return A new {@link IntPropertyKeyValue} object.
-         */
-        public final IntPropertyKeyValue atAbsolute(int keyValue) {
-            return new IntPropertyKeyValue(this, keyValue, 0f);
-        }
-
-        /**
-         * Creates an {@link IntPropertyKeyValue} object for a fraction of
-         * {@link IntSource#getMaxParentVisibleSize()}.
-         *
-         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
-         *                                       {@link IntSource#getMaxParentVisibleSize()} for
-         *                                       the key value.
-         * @return A new {@link IntPropertyKeyValue} object.
-         */
-        public final IntPropertyKeyValue atFraction(float fractionOfMaxParentVisibleSize) {
-            return new IntPropertyKeyValue(this, 0, fractionOfMaxParentVisibleSize);
-        }
-
-        /**
-         * Create an {@link IntPropertyKeyValue} object by multiplying the fraction with
-         * {@link IntSource#getMaxParentVisibleSize()} and adding offsetValue to it.
-         *
-         * @param offsetValue                    An offset integer value to be added to key value.
-         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
-         *                                       {@link IntSource#getMaxParentVisibleSize()} for
-         *                                       the key value.
-         * @return A new {@link IntPropertyKeyValue} object.
-         */
-        public final IntPropertyKeyValue at(int offsetValue,
-                float fractionOfMaxParentVisibleSize) {
-            return new IntPropertyKeyValue(this, offsetValue, fractionOfMaxParentVisibleSize);
-        }
-    }
-
-    /**
-     * Manages a list of {@link IntProperty}. App should override this class with a specific
-     * {@link IntProperty} subclass.
-     *
-     * @param <IntPropertyT> Type of {@link IntProperty} or subclass.
-     */
-    public abstract static class IntSource<IntPropertyT extends IntProperty>
-            extends ParallaxSource<IntPropertyT> {
-
-        private int[] mValues = new int[4];
-
-        /**
-         * Get index based property value.
-         *
-         * @param index Index of the property.
-         * @return Value of the property.
-         */
-        public final int getPropertyValue(int index) {
-            return mValues[index];
-        }
-
-        /**
-         * Set index based property value.
-         *
-         * @param index Index of the property.
-         * @param value Value of the property.
-         */
-        public final void setPropertyValue(int index, int value) {
-            mValues[index] = value;
-        }
-
-        /**
-         * Return the size of parent visible area, e.g. parent view's height if we are tracking Y
-         * position of a child. The size can be used to calculate key value using the provided
-         * fraction.
-         *
-         * @return Size of parent visible area.
-         * @see IntPropertyKeyValue
-         */
-        public abstract int getMaxParentVisibleSize();
-
-        @Override
-        public final IntPropertyT addProperty(String name) {
-            int newPropertyIndex = mProperties.size();
-            IntPropertyT property = createProperty(name, newPropertyIndex);
-            mProperties.add(property);
-            int size = mValues.length;
-            if (size == newPropertyIndex) {
-                int[] newValues = new int[size * 2];
-                for (int i = 0; i < size; i++) {
-                    newValues[i] = mValues[i];
-                }
-                mValues = newValues;
-            }
-            mValues[newPropertyIndex] = IntProperty.UNKNOWN_AFTER;
-            return property;
-        }
-
-        @Override
-        public final void verifyProperties() throws IllegalStateException {
-            if (mProperties.size() < 2) {
-                return;
-            }
-            int last = mProperties.get(0).getIntValue(this);
-            for (int i = 1; i < mProperties.size(); i++) {
-                int v = mProperties.get(i).getIntValue(this);
-                if (v < last) {
-                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
-                                    + " smaller than Property[%d]\"%s\"",
-                            i, mProperties.get(i).getName(),
-                            i - 1, mProperties.get(i - 1).getName()));
-                } else if (last == IntProperty.UNKNOWN_BEFORE && v == IntProperty.UNKNOWN_AFTER) {
-                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
-                                    + " UNKNOW_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
-                            i - 1, mProperties.get(i - 1).getName(),
-                            i, mProperties.get(i).getName()));
-                }
-                last = v;
-            }
-        }
-
-    }
-
-    /**
-     * Implementation of {@link PropertyKeyValue} for {@link IntProperty}.
-     */
-    public static class IntPropertyKeyValue extends PropertyKeyValue<IntProperty> {
-        private final int mValue;
-        private final float mFactionOfMax;
-
-        public IntPropertyKeyValue(IntProperty property, int value) {
-            this(property, value, 0f);
-        }
-
-        public IntPropertyKeyValue(IntProperty property, int value, float fractionOfMax) {
-            super(property);
-            mValue = value;
-            mFactionOfMax = fractionOfMax;
-        }
-
-        /**
-         * @return The key value of integer type.
-         */
-        public final int getKeyValue(IntSource source) {
-            return mFactionOfMax == 0 ? mValue : mValue + Math.round(source
-                    .getMaxParentVisibleSize() * mFactionOfMax);
-        }
-    }
-
-    /**
-     * FloatProperty provide access to an index based integer type property inside
-     * {@link FloatSource}. The FloatProperty typically represents UI element position inside
-     * {@link FloatSource}.
-     */
-    public static class FloatProperty extends Property<FloatSource, Float> {
-
-        /**
-         * Property value is unknown and it's above screen.
-         */
-        public static final float UNKNOWN_BEFORE = -Float.MAX_VALUE;
-        /**
-         * Property value is unknown and it's bellow screen.
-         */
-        public static final float UNKNOWN_AFTER = Float.MAX_VALUE;
-
-        private final int mIndex;
-
-        /**
-         * Constructor.
-         *
-         * @param name Name of this Property.
-         * @param index Index of this Property inside {@link FloatSource}.
-         */
-        public FloatProperty(String name, int index) {
-            super(Float.class, name);
-            mIndex = index;
-        }
-
-        @Override
-        public final Float get(FloatSource object) {
-            return getFloatValue(object);
-        }
-
-        @Override
-        public final void set(FloatSource object, Float value) {
-            setFloatValue(object, value);
-        }
-
-        final float getFloatValue(FloatSource source) {
-            return source.getPropertyValue(mIndex);
-        }
-
-        final void setFloatValue(FloatSource source, float value) {
-            source.setPropertyValue(mIndex, value);
-        }
-
-        /**
-         * @return Index of this Property in {@link FloatSource}.
-         */
-        public final int getIndex() {
-            return mIndex;
-        }
-
-        /**
-         * Creates an {@link FloatPropertyKeyValue} object for the absolute keyValue.
-         *
-         * @param keyValue The float key value.
-         * @return A new {@link FloatPropertyKeyValue} object.
-         */
-        public final FloatPropertyKeyValue atAbsolute(float keyValue) {
-            return new FloatPropertyKeyValue(this, keyValue, 0f);
-        }
-
-        /**
-         * Creates an {@link FloatPropertyKeyValue} object for a fraction of
-         * {@link FloatSource#getMaxParentVisibleSize()}.
-         *
-         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
-         *                                       {@link FloatSource#getMaxParentVisibleSize()} for
-         *                                       the key value.
-         * @return A new {@link FloatPropertyKeyValue} object.
-         */
-        public final FloatPropertyKeyValue atFraction(float fractionOfMaxParentVisibleSize) {
-            return new FloatPropertyKeyValue(this, 0, fractionOfMaxParentVisibleSize);
-        }
-
-        /**
-         * Create an {@link FloatPropertyKeyValue} object by multiplying the fraction with
-         * {@link FloatSource#getMaxParentVisibleSize()} and adding offsetValue to it.
-         *
-         * @param offsetValue                    An offset float value to be added to key value.
-         * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
-         *                                       {@link FloatSource#getMaxParentVisibleSize()} for
-         *                                       the key value.
-         * @return A new {@link FloatPropertyKeyValue} object.
-         */
-        public final FloatPropertyKeyValue at(float offsetValue,
-                float fractionOfMaxParentVisibleSize) {
-            return new FloatPropertyKeyValue(this, offsetValue, fractionOfMaxParentVisibleSize);
-        }
-    }
-
-    /**
-     * Manages a list of {@link FloatProperty}. App should override this class with a specific
-     * {@link FloatProperty} subclass.
-     *
-     * @param <FloatPropertyT> Type of {@link FloatProperty} or subclass.
-     */
-    public abstract static class FloatSource<FloatPropertyT extends FloatProperty> extends
-            ParallaxSource<FloatPropertyT> {
-
-        private float[] mValues = new float[4];
-
-        /**
-         * Get index based property value.
-         *
-         * @param index Index of the property.
-         * @return Value of the property.
-         */
-        public final float getPropertyValue(int index) {
-            return mValues[index];
-        }
-
-        /**
-         * Set index based property value.
-         *
-         * @param index Index of the property.
-         * @param value Value of the property.
-         */
-        public final void setPropertyValue(int index, float value) {
-            mValues[index] = value;
-        }
-
-        /**
-         * Return the size of parent visible area, e.g. parent view's height if we are tracking Y
-         * position of a child. The size can be used to calculate key value using the provided
-         * fraction.
-         *
-         * @return Size of parent visible area.
-         * @see FloatPropertyKeyValue
-         */
-        public abstract float getMaxParentVisibleSize();
-
-        @Override
-        public final FloatPropertyT addProperty(String name) {
-            int newPropertyIndex = mProperties.size();
-            FloatPropertyT property = createProperty(name, newPropertyIndex);
-            mProperties.add(property);
-            int size = mValues.length;
-            if (size == newPropertyIndex) {
-                float[] newValues = new float[size * 2];
-                for (int i = 0; i < size; i++) {
-                    newValues[i] = mValues[i];
-                }
-                mValues = newValues;
-            }
-            mValues[newPropertyIndex] = FloatProperty.UNKNOWN_AFTER;
-            return property;
-        }
-
-        @Override
-        public final void verifyProperties() throws IllegalStateException {
-            if (mProperties.size() < 2) {
-                return;
-            }
-            float last = mProperties.get(0).getFloatValue(this);
-            for (int i = 1; i < mProperties.size(); i++) {
-                float v = mProperties.get(i).getFloatValue(this);
-                if (v < last) {
-                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
-                                    + " smaller than Property[%d]\"%s\"",
-                            i, mProperties.get(i).getName(),
-                            i - 1, mProperties.get(i - 1).getName()));
-                } else if (last == FloatProperty.UNKNOWN_BEFORE && v
-                        == FloatProperty.UNKNOWN_AFTER) {
-                    throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
-                                    + " UNKNOW_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
-                            i - 1, mProperties.get(i - 1).getName(),
-                            i, mProperties.get(i).getName()));
-                }
-                last = v;
-            }
-        }
-
-    }
-
-    /**
-     * Implementation of {@link PropertyKeyValue} for {@link FloatProperty}.
-     */
-    public static class FloatPropertyKeyValue extends PropertyKeyValue<FloatProperty> {
-        private final float mValue;
-        private final float mFactionOfMax;
-
-        public FloatPropertyKeyValue(FloatProperty property, float value) {
-            this(property, value, 0f);
-        }
-
-        public FloatPropertyKeyValue(FloatProperty property, float value, float fractionOfMax) {
-            super(property);
-            mValue = value;
-            mFactionOfMax = fractionOfMax;
-        }
-
-        /**
-         * @return The key value.
-         */
-        public final float getKeyValue(FloatSource source) {
-            return mFactionOfMax == 0 ? mValue : mValue + source.getMaxParentVisibleSize()
-                    * mFactionOfMax;
-        }
-    }
-
-    final List<PropertyT> mProperties = new ArrayList<PropertyT>();
-    final List<PropertyT> mPropertiesReadOnly = Collections.unmodifiableList(mProperties);
-
-    /**
-     * @return A unmodifiable list of properties.
-     */
-    public final List<PropertyT> getProperties() {
-        return mPropertiesReadOnly;
-    }
-
-    /**
-     * Add a new Property in the ParallaxSource.
-     *
-     * @return Newly created Property.
-     */
-    public abstract PropertyT addProperty(String name);
-
-    /**
-     * Create a new Property object. App does not directly call this method.  See
-     * {@link #addProperty(String)}.
-     *
-     * @param index  Index of the property in this ParallaxSource object.
-     * @return Newly created Property object.
-     */
-    public abstract PropertyT createProperty(String name, int index);
-
-    /**
-     * Verify sanity of property values, throws RuntimeException if fails. The property values
-     * must be in ascending order. UNKNOW_BEFORE and UNKNOWN_AFTER are not allowed to be next to
-     * each other.
-     */
-    public abstract void verifyProperties() throws IllegalStateException;
-
-    /**
-     * Sets listener to monitor property value changes.
-     *
-     * @param listener The listener to set on {@link ParallaxSource} object.
-     */
-    public abstract void setListener(Listener listener);
-
-    /**
-     * This is used when the source is unaware of the updates and requires caller to update the
-     * values at the right time.
-     */
-    public void updateValues() {
-    }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java b/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
index fbed174..7020433 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
@@ -1,30 +1,30 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
+ * 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
+ *      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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
+
 package android.support.v17.leanback.widget;
 
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
 import android.view.animation.LinearInterpolator;
 
-import java.util.List;
-
 /**
- * ParallaxTarget is reponsible for updating the target through the {@link #update(float)} method.
- * {@link ParallaxEffect} transforms the values of {@link ParallaxSource}, which represents the
+ * ParallaxTarget is responsible for updating the target through the {@link #update(float)} method.
+ * {@link ParallaxEffect} transforms the values of {@link Parallax}, which represents the
  * current state of UI, into a float value between 0 and 1. That float value is passed into
  * {@link #update(float)} method.
- * @hide
  */
 public abstract class ParallaxTarget {
 
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxRecyclerViewSource.java b/v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
similarity index 83%
rename from v17/leanback/src/android/support/v17/leanback/widget/ParallaxRecyclerViewSource.java
rename to v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
index 6178421..20c40f9 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxRecyclerViewSource.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
@@ -1,16 +1,19 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
+ * 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
+ *      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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
+
 package android.support.v17.leanback.widget;
 
 import static android.support.v7.widget.RecyclerView.LayoutManager;
@@ -22,36 +25,31 @@
 import android.view.View;
 
 /**
- * Implementation of {@link ParallaxSource} class for {@link RecyclerView}. This class
+ * Implementation of {@link Parallax} class for {@link RecyclerView}. This class
  * allows users to track position of specific views inside {@link RecyclerView} relative to
  * itself. @see {@link ChildPositionProperty} for details.
- * @hide
  */
-public class ParallaxRecyclerViewSource extends
-        ParallaxSource.IntSource<ParallaxRecyclerViewSource.ChildPositionProperty> {
+public class RecyclerViewParallax extends
+        Parallax.IntParallax<RecyclerViewParallax.ChildPositionProperty> {
     RecyclerView mRecylerView;
-    Listener mListener;
     boolean mIsVertical;
 
     OnScrollListener mOnScrollListener = new OnScrollListener() {
         @Override
         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-            for (ChildPositionProperty prop: getProperties()) {
-                prop.updateValue(ParallaxRecyclerViewSource.this);
-            }
-            mListener.onPropertiesChanged(ParallaxRecyclerViewSource.this);
+            updateValues();
         }
     };
 
     /**
-     * Subclass of {@link ParallaxSource.IntProperty}. Using this Property, users can track a
+     * Subclass of {@link Parallax.IntProperty}. Using this Property, users can track a
      * RecylerView child's position inside recyclerview. i.e.
      *
      * tracking_pos = view.top + fraction * view.height() + offset
      *
      * This way we can track top using fraction 0 and bottom using fraction 1.
      */
-    public static final class ChildPositionProperty extends ParallaxSource.IntProperty {
+    public static final class ChildPositionProperty extends Parallax.IntProperty {
         int mAdapterPosition;
         int mViewId;
         int mOffset;
@@ -137,7 +135,7 @@
             return mFraction;
         }
 
-        void updateValue(ParallaxRecyclerViewSource source) {
+        void updateValue(RecyclerViewParallax source) {
             RecyclerView recyclerView = source.mRecylerView;
             ViewHolder viewHolder = recyclerView == null ? null
                     : recyclerView.findViewHolderForAdapterPosition(mAdapterPosition);
@@ -190,12 +188,7 @@
     }
 
     @Override
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    @Override
-    public int getMaxParentVisibleSize() {
+    public int getMaxValue() {
         if (mRecylerView == null) {
             return 0;
         }
@@ -203,7 +196,7 @@
     }
 
     /**
-     * Set RecyclerView that this ParallaxSource will register onScrollListener.
+     * Set RecyclerView that this Parallax will register onScrollListener.
      * @param recyclerView RecyclerView to register onScrollListener.
      */
     public void setRecyclerView(RecyclerView recyclerView) {
@@ -229,11 +222,9 @@
     @Override
     public void updateValues() {
         for (ChildPositionProperty prop: getProperties()) {
-            prop.updateValue(ParallaxRecyclerViewSource.this);
+            prop.updateValue(RecyclerViewParallax.this);
         }
-        if (mListener != null) {
-            mListener.onPropertiesChanged(ParallaxRecyclerViewSource.this);
-        }
+        super.updateValues();
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
index 0300c6f..04a384a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
@@ -13,7 +13,9 @@
  */
 package android.support.v17.leanback.widget;
 
+import android.support.annotation.NonNull;
 import android.support.v4.util.CircularIntArray;
+import android.support.v7.widget.RecyclerView;
 
 import java.io.PrintWriter;
 
@@ -131,6 +133,36 @@
     }
 
     @Override
+    public void collectAdjacentPrefetchPositions(int fromLimit, int da,
+        @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        int indexToPrefetch;
+        int nearestEdge;
+        if (mReversedFlow ? da > 0 : da < 0) {
+            // prefetch next prepend, lower index number
+            if (getFirstVisibleIndex() == 0) {
+                return; // no remaining items to prefetch
+            }
+
+            indexToPrefetch = getStartIndexForPrepend();
+            nearestEdge = mProvider.getEdge(mFirstVisibleIndex)
+                    + (mReversedFlow ? mSpacing : -mSpacing);
+        } else {
+            // prefetch next append, higher index number
+            if (getLastVisibleIndex() == mProvider.getCount() - 1) {
+                return; // no remaining items to prefetch
+            }
+
+            indexToPrefetch = getStartIndexForAppend();
+            int itemSizeWithSpace = mProvider.getSize(mLastVisibleIndex) + mSpacing;
+            nearestEdge = mProvider.getEdge(mLastVisibleIndex)
+                    + (mReversedFlow ? -itemSizeWithSpace : itemSizeWithSpace);
+        }
+
+        int distance = Math.abs(nearestEdge - fromLimit);
+        layoutPrefetchRegistry.addPosition(indexToPrefetch, distance);
+    }
+
+    @Override
     public final CircularIntArray[] getItemPositionsInRows(int startPos, int endPos) {
         // all items are on the same row:
         mTmpItemPositionsInRows[0].clear();
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java b/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
new file mode 100644
index 0000000..29d778c
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+
+/**
+ * Activity transition will change transitionVisibility multiple times even the view is not
+ * running transition, which causes visual flickering during activity return transition.
+ * This class disables setTransitionVisibility() to avoid the problem.
+ * @hide
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+    public VideoSurfaceView(Context context) {
+        super(context);
+    }
+
+    public VideoSurfaceView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    /**
+     * Overrides hidden method View.setTransitionVisibility() to disable visibility changes
+     * in activity transition.
+     */
+    public void setTransitionVisibility(int visibility) {
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
index 31863a9..7725bf3 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
@@ -59,7 +59,7 @@
 
     final static String DATE_FORMAT = "MM/dd/yyyy";
     final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
-    PickerConstant mConstant;
+    PickerUtility.DateConstant mConstant;
 
     Calendar mMinDate;
     Calendar mMaxDate;
@@ -179,23 +179,13 @@
         return mDatePickerFormat;
     }
 
-    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
-        if (oldCalendar == null) {
-            return Calendar.getInstance(locale);
-        } else {
-            final long currentTimeMillis = oldCalendar.getTimeInMillis();
-            Calendar newCalendar = Calendar.getInstance(locale);
-            newCalendar.setTimeInMillis(currentTimeMillis);
-            return newCalendar;
-        }
-    }
-
     private void updateCurrentLocale() {
-        mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources());
-        mTempDate = getCalendarForLocale(mTempDate, mConstant.locale);
-        mMinDate = getCalendarForLocale(mMinDate, mConstant.locale);
-        mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale);
-        mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
+        mConstant = PickerUtility.getDateConstantInstance(Locale.getDefault(),
+                getContext().getResources());
+        mTempDate = PickerUtility.getCalendarForLocale(mTempDate, mConstant.locale);
+        mMinDate = PickerUtility.getCalendarForLocale(mMinDate, mConstant.locale);
+        mMaxDate = PickerUtility.getCalendarForLocale(mMaxDate, mConstant.locale);
+        mCurrentDate = PickerUtility.getCalendarForLocale(mCurrentDate, mConstant.locale);
 
         if (mMonthColumn != null) {
             mMonthColumn.setStaticLabels(mConstant.months);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
index 55ec6de2..28d7aeb 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
@@ -133,6 +133,7 @@
         super(context, attrs, defStyleAttr);
         // Make it enabled and clickable to receive Click event.
         setEnabled(true);
+        setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
 
         mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha);
         mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha);
@@ -147,7 +148,6 @@
         LayoutInflater inflater = LayoutInflater.from(getContext());
         mRootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true);
         mPickerView = (ViewGroup) mRootView.findViewById(R.id.picker);
-
     }
 
     /**
@@ -196,6 +196,12 @@
             columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
             // Width is dynamic, so has fixed size is false.
             columnView.setHasFixedSize(false);
+            columnView.setFocusable(isActivated());
+            // Setting cache size to zero in order to rebind item views when picker widget becomes
+            // activated. Rebinding is necessary to update the alphas when the columns are expanded
+            // as a result of the picker getting activated, otherwise the cached views with the
+            // wrong alphas could be laid out.
+            columnView.setItemViewCacheSize(0);
             mColumnViews.add(columnView);
 
             // add view to root
@@ -549,13 +555,40 @@
 
     @Override
     public void setActivated(boolean activated) {
-        if (activated != isActivated()) {
+        if (activated == isActivated()) {
             super.setActivated(activated);
-            updateColumnSize();
-            updateItemFocusable();
-        } else {
-            super.setActivated(activated);
+            return;
         }
+        super.setActivated(activated);
+        boolean hadFocus = hasFocus();
+        int column = getSelectedColumn();
+        // To avoid temporary focus loss in both the following cases, we set Picker's flag to
+        // FOCUS_BEFORE_DESCENDANTS first, and then back to FOCUS_AFTER_DESCENDANTS once done with
+        // the focus logic.
+        // 1. When changing from activated to deactivated, the Picker should grab the focus
+        // back if it's focusable. However, calling requestFocus on it will transfer the focus down
+        // to its children if it's flag is FOCUS_AFTER_DESCENDANTS.
+        // 2. When changing from deactivated to activated, while setting focusable flags on each
+        // column VerticalGridView, that column will call requestFocus (regardless of which column
+        // is the selected column) since the currently focused view (Picker) has a flag of
+        // FOCUS_AFTER_DESCENDANTS.
+        setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
+        if (!activated && hadFocus && isFocusable()) {
+            // When picker widget that originally had focus is deactivated and it is focusable, we
+            // should not pass the focus down to the children. The Picker itself will capture focus.
+            requestFocus();
+        }
+
+        for (int i = 0; i < getColumnsCount(); i++) {
+            mColumnViews.get(i).setFocusable(activated);
+        }
+
+        updateColumnSize();
+        updateItemFocusable();
+        if (activated && hadFocus && (column >= 0)) {
+            mColumnViews.get(column).requestFocus();
+        }
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
     }
 
     @Override
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
deleted file mode 100644
index cfb704f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.widget.picker;
-
-import android.content.res.Resources;
-import android.support.v17.leanback.R;
-
-import java.text.DateFormatSymbols;
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Date/Time Picker related constants
- */
-class PickerConstant {
-
-    public final String[] months;
-    public final String[] days;
-    public final String[] hours12;
-    public final String[] hours24;
-    public final String[] minutes;
-    public final String[] ampm;
-    public final String dateSeparator;
-    public final String timeSeparator;
-    public final Locale locale;
-
-    public PickerConstant(Locale locale, Resources resources) {
-        this.locale = locale;
-        DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
-        months = symbols.getShortMonths();
-        Calendar calendar = Calendar.getInstance(locale);
-        days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
-                calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
-        hours12 = createStringIntArrays(1, 12, "%02d");
-        hours24 = createStringIntArrays(0, 23, "%02d");
-        minutes = createStringIntArrays(0, 59, "%02d");
-        ampm = symbols.getAmPmStrings();
-        dateSeparator = resources.getString(R.string.lb_date_separator);
-        timeSeparator = resources.getString(R.string.lb_time_separator);
-    }
-
-
-    public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
-        String[] array = new String[lastNumber - firstNumber + 1];
-        for (int i = firstNumber; i <= lastNumber; i++) {
-            if (format != null) {
-                array[i - firstNumber] = String.format(format, i);
-            } else {
-                array[i - firstNumber] = String.valueOf(i);
-            }
-        }
-        return array;
-    }
-
-
-}
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
new file mode 100644
index 0000000..1e3a28f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget.picker;
+
+import android.content.res.Resources;
+import android.support.v17.leanback.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Utility class that provides Date/Time related constants as well as locale-specific calendar for
+ * both {@link DatePicker} and {@link TimePicker}.
+ */
+class PickerUtility {
+
+    public static class DateConstant {
+        public final Locale locale;
+        public final String[] months;
+        public final String[] days;
+        public final String dateSeparator;
+
+        private DateConstant(Locale locale, Resources resources) {
+            this.locale = locale;
+            DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+            months = symbols.getShortMonths();
+            Calendar calendar = Calendar.getInstance(locale);
+            days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
+                    calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
+            dateSeparator = resources.getString(R.string.lb_date_separator);
+        }
+    }
+
+    public static class TimeConstant {
+        public final Locale locale;
+        public final String[] hours12;
+        public final String[] hours24;
+        public final String[] minutes;
+        public final String[] ampm;
+        public final String timeSeparator;
+
+        private TimeConstant(Locale locale, Resources resources) {
+            this.locale = locale;
+            DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+            hours12 = createStringIntArrays(1, 12, "%02d");
+            hours24 = createStringIntArrays(0, 23, "%02d");
+            minutes = createStringIntArrays(0, 59, "%02d");
+            ampm = symbols.getAmPmStrings();
+            timeSeparator = resources.getString(R.string.lb_time_separator);
+        }
+    }
+
+    public static DateConstant getDateConstantInstance(Locale locale, Resources resources) {
+        return new DateConstant(locale, resources);
+    }
+
+    public static TimeConstant getTimeConstantInstance(Locale locale, Resources resources) {
+        return new TimeConstant(locale, resources);
+    }
+
+
+    public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
+        String[] array = new String[lastNumber - firstNumber + 1];
+        for (int i = firstNumber; i <= lastNumber; i++) {
+            if (format != null) {
+                array[i - firstNumber] = String.format(format, i);
+            } else {
+                array[i - firstNumber] = String.valueOf(i);
+            }
+        }
+        return array;
+    }
+
+    public static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+        if (oldCalendar == null) {
+            return Calendar.getInstance(locale);
+        } else {
+            final long currentTimeMillis = oldCalendar.getTimeInMillis();
+            Calendar newCalendar = Calendar.getInstance(locale);
+            newCalendar.setTimeInMillis(currentTimeMillis);
+            return newCalendar;
+        }
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
new file mode 100644
index 0000000..a0452f5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget.picker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * {@link TimePicker} is a direct subclass of {@link Picker}.
+ * <p>
+ * This class is a widget for selecting time and displays it according to the formatting for the
+ * current system locale. The time can be selected by hour, minute, and AM/PM picker columns.
+ * The AM/PM mode is activated only when the 12 hour format is desired by setting
+ * {@code is24HourFormat} attribute to false. Otherwise, TimePicker displays only two columns for a
+ * 24 hour time format.
+ * <p>
+ * This widget can show the current time as the initial value if {@code useCurrentTime} is set to
+ * true. Each individual time picker field can be set  set at any time by calling
+ * {@link #setHour(int)}, {@link #setMinute(int)} using 24-hour time format. The time format can
+ * also be changed at any time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column
+ * will be activated or deactivated according to the given format.
+ *
+ * @attr ref R.styleable#lbTimePicker_is24HourFormat
+ * @attr ref R.styleable#lbTimePicker_useCurrentTime
+ */
+public class TimePicker extends Picker {
+
+    static final String TAG = "TimePicker";
+
+    private static final int AM_INDEX = 0;
+    private static final int PM_INDEX = 1;
+
+    private static final int HOURS_IN_HALF_DAY = 12;
+    PickerColumn mHourColumn;
+    PickerColumn mMinuteColumn;
+    PickerColumn mAmPmColumn;
+    private ViewGroup mPickerView;
+    private View mAmPmSeparatorView;
+    int mColHourIndex;
+    int mColMinuteIndex;
+    int mColAmPmIndex;
+
+    private final PickerUtility.TimeConstant mConstant;
+
+    private boolean mIs24hFormat;
+
+    private int mCurrentHour;
+    private int mCurrentMinute;
+    private int mCurrentAmPmIndex;
+
+    /**
+     * Constructor called when inflating a TimePicker widget. This version uses a default style of
+     * 0, so the only attribute values applied are those in the Context's Theme and the given
+     * AttributeSet.
+     *
+     * @param context the context this TimePicker widget is associated with through which we can
+     *                access the current theme attributes and resources
+     * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+     */
+    public TimePicker(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Constructor called when inflating a TimePicker widget.
+     *
+     * @param context the context this TimePicker widget is associated with through which we can
+     *                access the current theme attributes and resources
+     * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+     * @param defStyleAttr An attribute in the current theme that contains a reference to a style
+     *                     resource that supplies default values for the widget. Can be 0 to not
+     *                     look for defaults.
+     */
+    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(),
+                context.getResources());
+
+        setSeparator(mConstant.timeSeparator);
+        mPickerView = (ViewGroup) findViewById(R.id.picker);
+        final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+                R.styleable.lbTimePicker);
+        mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat, false);
+        boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime,
+                true);
+
+        updateColumns(getTimePickerFormat());
+
+        // The column range for the minute and AM/PM column is static and does not change, whereas
+        // the hour column range can change depending on whether 12 or 24 hour format is set at
+        // any given time.
+        updateHourColumn(false);
+        updateMin(mMinuteColumn, 0);
+        updateMax(mMinuteColumn, 59);
+
+        updateMin(mAmPmColumn, 0);
+        updateMax(mAmPmColumn, 1);
+
+        updateAmPmColumn();
+
+        if (useCurrentTime) {
+            Calendar currentDate = PickerUtility.getCalendarForLocale(null,
+                    mConstant.locale);
+            setHour(currentDate.get(Calendar.HOUR_OF_DAY));
+            setMinute(currentDate.get(Calendar.MINUTE));
+        }
+    }
+
+    private static boolean updateMin(PickerColumn column, int value) {
+        if (value != column.getMinValue()) {
+            column.setMinValue(value);
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean updateMax(PickerColumn column, int value) {
+        if (value != column.getMaxValue()) {
+            column.setMaxValue(value);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     *
+     * @return the time picker format string based on the current system locale and the layout
+     *         direction
+     */
+    private String getTimePickerFormat() {
+        // Obtain the time format string per the current locale (e.g. h:mm a)
+        String hmaPattern  = ((SimpleDateFormat) DateFormat
+                .getTimeInstance(DateFormat.SHORT, mConstant.locale)).toPattern();
+        boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View
+                .LAYOUT_DIRECTION_RTL;
+        boolean isAmPmAtEnd = hmaPattern.indexOf("a") > hmaPattern.indexOf("m");
+        // Hour will always appear to the left of minutes regardless of layout direction.
+        String timePickerFormat = isRTL ? "mh" : "hm";
+
+        return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat);
+    }
+
+    private void updateColumns(String timePickerFormat) {
+        if (TextUtils.isEmpty(timePickerFormat)) {
+            timePickerFormat = "hma";
+        }
+        timePickerFormat = timePickerFormat.toUpperCase();
+
+        mHourColumn = mMinuteColumn = mAmPmColumn = null;
+        mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1;
+
+        ArrayList<PickerColumn> columns = new ArrayList<>(3);
+        for (int i = 0; i < timePickerFormat.length(); i++) {
+            switch (timePickerFormat.charAt(i)) {
+                case 'H':
+                    columns.add(mHourColumn = new PickerColumn());
+                    mHourColumn.setStaticLabels(mConstant.hours24);
+                    mColHourIndex = i;
+                    break;
+                case 'M':
+                    columns.add(mMinuteColumn = new PickerColumn());
+                    mMinuteColumn.setStaticLabels(mConstant.minutes);
+                    mColMinuteIndex = i;
+                    break;
+                case 'A':
+                    columns.add(mAmPmColumn = new PickerColumn());
+                    mAmPmColumn.setStaticLabels(mConstant.ampm);
+                    mColAmPmIndex = i;
+                    updateMin(mAmPmColumn, 0);
+                    updateMax(mAmPmColumn, 1);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid time picker format.");
+            }
+        }
+        setColumns(columns);
+        mAmPmSeparatorView = mPickerView.getChildAt(mColAmPmIndex == 0 ? 1 :
+                (2 * mColAmPmIndex - 1));
+    }
+
+    /**
+     * Updates the range in the hour column and notifies column changed if notifyChanged is true.
+     * Hour column can have either [0-23] or [1-12] depending on whether the 24 hour format is set
+     * or not.
+     *
+     * @param notifyChanged {code true} if we should notify data set changed on the hour column,
+     *                      {@code false} otherwise.
+     */
+    private void updateHourColumn(boolean notifyChanged) {
+        updateMin(mHourColumn, mIs24hFormat ? 0 : 1);
+        updateMax(mHourColumn, mIs24hFormat ? 23 : 12);
+        if (notifyChanged) {
+            setColumnAt(mColHourIndex, mHourColumn);
+        }
+    }
+
+    /**
+     * Updates AM/PM column depending on whether the 24 hour format is set or not. The visibility of
+     * this column is set to {@code GONE} for a 24 hour format, and {@code VISIBLE} in 12 hour
+     * format. This method also updates the value of this column for a 12 hour format.
+     */
+    private void updateAmPmColumn() {
+        if (mIs24hFormat) {
+            mColumnViews.get(mColAmPmIndex).setVisibility(GONE);
+            mAmPmSeparatorView.setVisibility(GONE);
+        } else {
+            mColumnViews.get(mColAmPmIndex).setVisibility(VISIBLE);
+            mAmPmSeparatorView.setVisibility(VISIBLE);
+            setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false);
+        }
+    }
+
+    /**
+     * Sets the currently selected hour using a 24-hour time.
+     *
+     * @param hour the hour to set, in the range (0-23)
+     * @see #getHour()
+     */
+    public void setHour(int hour) {
+        if (hour < 0 || hour > 23) {
+            throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in");
+        }
+        mCurrentHour = hour;
+        if (!mIs24hFormat) {
+            if (mCurrentHour >= HOURS_IN_HALF_DAY) {
+                mCurrentAmPmIndex = PM_INDEX;
+                if (mCurrentHour > HOURS_IN_HALF_DAY) {
+                    mCurrentHour -= HOURS_IN_HALF_DAY;
+                }
+            } else {
+                mCurrentAmPmIndex = AM_INDEX;
+                if (mCurrentHour == 0) {
+                    mCurrentHour = HOURS_IN_HALF_DAY;
+                }
+            }
+            updateAmPmColumn();
+        }
+        setColumnValue(mColHourIndex, mCurrentHour, false);
+    }
+
+    /**
+     * Returns the currently selected hour using 24-hour time.
+     *
+     * @return the currently selected hour in the range (0-23)
+     * @see #setHour(int)
+     */
+    public int getHour() {
+        if (mIs24hFormat) {
+            return mCurrentHour;
+        }
+        if (mCurrentAmPmIndex == AM_INDEX) {
+            return mCurrentHour % HOURS_IN_HALF_DAY;
+        }
+        return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+    }
+
+    /**
+     * Sets the currently selected minute.
+     *
+     * @param minute the minute to set, in the range (0-59)
+     * @see #getMinute()
+     */
+    public void setMinute(int minute) {
+        if (mCurrentMinute == minute) {
+            return;
+        }
+        if (minute < 0 || minute > 59) {
+            throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range.");
+        }
+        mCurrentMinute = minute;
+        setColumnValue(mColMinuteIndex, mCurrentMinute, false);
+    }
+
+    /**
+     * Returns the currently selected minute.
+     *
+     * @return the currently selected minute, in the range (0-59)
+     * @see #setMinute(int)
+     */
+    public int getMinute() {
+        return mCurrentMinute;
+    }
+
+    /**
+     * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker.
+     *
+     * @param is24Hour {@code true} to display in 24-hour mode,
+     *                 {@code false} ti display in 12-hour mode with AM/PM.
+     * @see #is24Hour()
+     */
+    public void setIs24Hour(boolean is24Hour) {
+        if (mIs24hFormat == is24Hour) {
+            return;
+        }
+        // the ordering of these statements is important
+        int currentHour = getHour();
+        mIs24hFormat = is24Hour;
+        updateHourColumn(true);
+        setHour(currentHour);
+        updateAmPmColumn();
+    }
+
+    /**
+     * @return {@code true} if this widget displays time in 24-hour mode,
+     *         {@code false} otherwise.
+     *
+     * @see #setIs24Hour(boolean)
+     */
+    public boolean is24Hour() {
+        return mIs24hFormat;
+    }
+
+    /**
+     * Only meaningful for a 12-hour time.
+     *
+     * @return {@code true} if the currently selected time is in PM,
+     *         {@code false} if the currently selected time in in AM.
+     */
+    public boolean isPm() {
+        return (mCurrentAmPmIndex == PM_INDEX);
+    }
+
+    @Override
+    public void onColumnValueChanged(int columnIndex, int newValue) {
+        if (columnIndex == mColHourIndex) {
+            mCurrentHour = newValue;
+        } else if (columnIndex == mColMinuteIndex) {
+            mCurrentMinute = newValue;
+        } else if (columnIndex == mColAmPmIndex) {
+            mCurrentAmPmIndex = newValue;
+        } else {
+            throw new IllegalArgumentException("Invalid column index.");
+        }
+    }
+}
diff --git a/v17/leanback/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index 6e6b157..c2c24d0 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -33,12 +33,20 @@
         <activity android:name="android.support.v17.leanback.widget.GridActivity"
                   android:exported="true" />
 
+        <activity android:name="android.support.v17.leanback.widget.TimePickerActivity"
+                  android:theme="@style/Theme.Leanback"
+                  android:exported="true" />
+
+        <activity android:name="android.support.v17.leanback.widget.DatePickerActivity"
+                  android:theme="@style/Theme.Leanback"
+                  android:exported="true" />
+
         <activity
                 android:name="android.support.v17.leanback.app.wizard.GuidedStepAttributesTestActivity"
                 android:theme="@style/Theme.Leanback.GuidedStep"
                 android:exported="true" />
 
-        <activity android:name="android.support.v17.leanback.app.RowsFragmentTestActivity"
+        <activity android:name="android.support.v17.leanback.app.SingleFragmentTestActivity"
             android:theme="@style/Theme.Leanback"
             android:exported="true" />
 
@@ -46,11 +54,7 @@
                   android:theme="@style/Theme.Leanback.Browse"
                   android:exported="true" />
 
-        <activity android:name="android.support.v17.leanback.app.DetailsFragmentTestActivity"
-                  android:theme="@style/Theme.Leanback"
-                  android:exported="true" />
-
-        <activity android:name="android.support.v17.leanback.app.RowsSupportFragmentTestActivity"
+        <activity android:name="android.support.v17.leanback.app.SingleSupportFragmentTestActivity"
             android:theme="@style/Theme.Leanback"
             android:exported="true" />
 
@@ -66,31 +70,6 @@
                   android:theme="@style/Theme.Leanback.GuidedStep"
                   android:exported="true" />
 
-        <activity android:name="android.support.v17.leanback.app.PlaybackTestActivity"
-                  android:theme="@style/Theme.Leanback"
-                  android:exported="true" />
-
-        <activity android:name="android.support.v17.leanback.app.PlaybackSupportTestActivity"
-                  android:theme="@style/Theme.Leanback"
-                  android:exported="true" />
-
-        <activity android:name="android.support.v17.leanback.app.PlaybackOverlayTestActivity"
-                  android:theme="@style/Theme.Leanback"
-                  android:exported="true" />
-        <activity
-            android:name="android.support.v17.leanback.app.VerticalGridFragmentTest$ImmediateRemoveFragmentActivity"
-            android:exported="true"
-            android:theme="@style/Theme.Leanback.VerticalGrid" />
-
-        <activity
-            android:name="android.support.v17.leanback.app.VerticalGridSupportFragmentTest$ImmediateRemoveFragmentActivity"
-            android:exported="true"
-            android:theme="@style/Theme.Leanback.VerticalGrid" />
-
-        <activity android:name="android.support.v17.leanback.app.VideoFragmentTestActivity"
-            android:theme="@style/Theme.Leanback"
-            android:exported="true" />
-
         <activity android:name="android.support.v17.leanback.app.TestActivity"
             android:theme="@style/Theme.Leanback"
             android:exported="true" />
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
index ecae656..34ace00 100755
--- a/v17/leanback/tests/generatev4.py
+++ b/v17/leanback/tests/generatev4.py
@@ -19,11 +19,14 @@
 
 print "Generate v4 fragment related code for leanback"
 
-files = ['BrowseTest', 'GuidedStepTest', 'RowsTest']
+####### generate XXXTestFragment classes #######
+
+files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest', 'DetailsTest']
 
 cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
       'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
-      'GuidedStepTest', 'GuidedStep', 'RowsTest']
+      'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video',
+      'DetailsTest']
 
 for w in files:
     print "copy {}Fragment to {}SupportFragment".format(w, w)
@@ -44,7 +47,9 @@
     file.close()
     outfile.close()
 
-testcls = ['GuidedStep']
+####### generate XXXFragmentTestBase classes #######
+
+testcls = ['GuidedStep', 'Single']
 
 for w in testcls:
     print "copy {}FrgamentTestBase to {}SupportFragmentTestBase".format(w, w)
@@ -68,7 +73,9 @@
     file.close()
     outfile.close()
 
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Rows']
+####### generate XXXFragmentTest classes #######
+
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
 
 for w in testcls:
     print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
@@ -83,6 +90,7 @@
         for w in cls:
             line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
         for w in testcls:
+            line = line.replace('SingleFragmentTestBase', 'SingleSupportFragmentTestBase')
             line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
             line = line.replace('{}FragmentTest'.format(w), '{}SupportFragmentTest'.format(w))
             line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
@@ -91,11 +99,14 @@
         line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
 	line = line.replace('extends Activity', 'extends FragmentActivity')
 	line = line.replace('Activity.this.getFragmentManager', 'Activity.this.getSupportFragmentManager')
+	line = line.replace('mActivity.getFragmentManager', 'mActivity.getSupportFragmentManager')
         outfile.write(line)
     file.close()
     outfile.close()
 
-testcls = ['Browse', 'GuidedStep', 'Rows']
+
+####### generate XXXTestActivity classes #######
+testcls = ['Browse', 'GuidedStep', 'Single']
 
 for w in testcls:
     print "copy {}FragmentTestActivity to {}SupportFragmentTestActivity".format(w, w)
@@ -114,6 +125,8 @@
     file.close()
     outfile.close()
 
+####### generate Float parallax test #######
+
 print "copy ParallaxIntEffectTest to ParallaxFloatEffectTest"
 file = open('java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java', 'r')
 outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java', 'w')
@@ -121,11 +134,11 @@
 outfile.write("/* This file is auto-generated from ParallaxIntEffectTest.java.  DO NOT MODIFY. */\n\n")
 for line in file:
     line = line.replace('IntEffect', 'FloatEffect')
-    line = line.replace('IntSource', 'FloatSource')
+    line = line.replace('IntParallax', 'FloatParallax')
     line = line.replace('IntProperty', 'FloatProperty')
     line = line.replace('IntValue', 'FloatValue')
     line = line.replace('intValue()', 'floatValue()')
-    line = line.replace('int getMaxParentVisibleSize', 'float getMaxParentVisibleSize')
+    line = line.replace('int getMaxValue', 'float getMaxValue')
     line = line.replace('int screenMax', 'float screenMax')
     line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
     line = line.replace('(int)', '(float)')
@@ -134,17 +147,18 @@
 outfile.close()
 
 
-print "copy ParallaxIntSourceTest to ParallaxFloatSourceTest"
-file = open('java/android/support/v17/leanback/widget/ParallaxIntSourceTest.java', 'r')
-outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatSourceTest.java', 'w')
+print "copy ParallaxIntTest to ParallaxFloatTest"
+file = open('java/android/support/v17/leanback/widget/ParallaxIntTest.java', 'r')
+outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatTest.java', 'w')
 outfile.write("// CHECKSTYLE:OFF Generated code\n")
-outfile.write("/* This file is auto-generated from ParallaxIntSourceTest.java.  DO NOT MODIFY. */\n\n")
+outfile.write("/* This file is auto-generated from ParallaxIntTest.java.  DO NOT MODIFY. */\n\n")
 for line in file:
-    line = line.replace('IntSource', 'FloatSource')
+    line = line.replace('ParallaxIntTest', 'ParallaxFloatTest')
+    line = line.replace('IntParallax', 'FloatParallax')
     line = line.replace('IntProperty', 'FloatProperty')
     line = line.replace('IntValue', 'FloatValue')
     line = line.replace('intValue()', 'floatValue()')
-    line = line.replace('int getMaxParentVisibleSize', 'float getMaxParentVisibleSize')
+    line = line.replace('int getMaxValue', 'float getMaxValue')
     line = line.replace('int screenMax', 'float screenMax')
     line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
     line = line.replace('(int)', '(float)')
@@ -152,6 +166,7 @@
 file.close()
 outfile.close()
 
+####### generate glue support test #######
 
 print "copy PlaybackControlGlueTest to PlaybackControlSupportGlueTest"
 file = open('java/android/support/v17/leanback/app/PlaybackControlGlueTest.java', 'r')
@@ -160,6 +175,8 @@
 outfile.write("/* This file is auto-generated from PlaybackControlGlueTest.java.  DO NOT MODIFY. */\n\n")
 for line in file:
     line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue')
+    line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
+    line = line.replace('PlaybackGlueHostOld', 'PlaybackSupportGlueHostOld')
     outfile.write(line)
 file.close()
 outfile.close()
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 c24a7e5..b70cc28 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,60 +15,138 @@
  */
 package android.support.v17.leanback.app;
 
+import static junit.framework.TestCase.assertFalse;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
+import android.animation.PropertyValuesHolder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.v17.leanback.R;
-import android.support.v17.leanback.graphics.CompositeDrawable;
 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.widget.DetailsParallax;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+import android.support.v17.leanback.widget.RecyclerViewParallax;
 import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.KeyEvent;
 import android.view.View;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /**
- * Unit tests for {@link DetailsTestFragment}.
+ * Unit tests for {@link DetailsFragment}.
  */
 @RunWith(JUnit4.class)
 @MediumTest
-public class DetailsFragmentTest {
+public class DetailsFragmentTest extends SingleFragmentTestBase {
 
-    @Rule
-    public ActivityTestRule<DetailsFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(DetailsFragmentTestActivity.class, false, false);
-    private DetailsFragmentTestActivity mActivity;
+    static final int PARALLAX_VERTICAL_OFFSET = -300;
+
+    public static class DetailsFragmentParallax extends DetailsTestFragment {
+
+        private DetailsParallaxDrawable mParallaxDrawable;
+
+        public DetailsFragmentParallax() {
+            super();
+            mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            Drawable coverDrawable = new FitWidthBitmapDrawable();
+            mParallaxDrawable = new DetailsParallaxDrawable(
+                    getActivity(),
+                    getParallax(),
+                    coverDrawable,
+                    new ParallaxTarget.PropertyValuesHolderTarget(
+                            coverDrawable,
+                            PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
+                    )
+            );
+
+            BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
+            backgroundManager.attach(getActivity().getWindow());
+            backgroundManager.setDrawable(mParallaxDrawable);
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            setItem(new PhotoItem("Hello world", "Fake content goes here",
+                    android.support.v17.leanback.test.R.drawable.spiderman));
+        }
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
+        }
+
+        DetailsParallaxDrawable getParallaxDrawable() {
+            return mParallaxDrawable;
+        }
+    }
+
+    @Test
+    public void parallaxSetupTest() {
+        launchAndWaitActivity(DetailsFragmentTest.DetailsFragmentParallax.class,
+                new SingleFragmentTestBase.Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        double delta = 0.0002;
+        DetailsParallax dpm = ((DetailsFragment) mActivity.getTestFragment()).getParallax();
+
+        RecyclerViewParallax.ChildPositionProperty frameTop =
+                (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
+        assertEquals(0f, frameTop.getFraction(), delta);
+        assertEquals(0f, frameTop.getAdapterPosition(), delta);
+
+
+        RecyclerViewParallax.ChildPositionProperty frameBottom =
+                (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
+        assertEquals(1f, frameBottom.getFraction(), delta);
+        assertEquals(0f, frameBottom.getAdapterPosition(), delta);
+    }
 
     @Test
     public void parallaxTest() throws Throwable {
-        final int mDefaultVerticalOffset = -300;
-        Intent intent = new Intent();
-        intent.putExtra(DetailsTestFragment.VERTICAL_OFFSET, mDefaultVerticalOffset);
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(DetailsFragmentParallax.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
 
-        final DetailsTestFragment detailsFragment = mActivity.getDetailsFragment();
-        DetailsBackgroundParallaxHelper parallaxHelper = detailsFragment.getParallaxHelper();
-        final CompositeDrawable drawable = (CompositeDrawable) parallaxHelper.getDrawable();
+        final DetailsFragmentParallax detailsFragment =
+                (DetailsFragmentParallax) mActivity.getTestFragment();
+        DetailsParallaxDrawable drawable =
+                detailsFragment.getParallaxDrawable();
         final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
-                (drawable.getChildAt(0).getDrawable());
+                drawable.getCoverDrawable();
 
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
-                return mActivity.getDetailsFragment().getRowsFragment().getAdapter().size() > 1;
+                return detailsFragment.getRowsFragment().getAdapter() != null
+                        && detailsFragment.getRowsFragment().getAdapter().size() > 1;
             }
         });
 
-        final VerticalGridView verticalGridView = mActivity.getDetailsFragment()
-                .getRowsFragment().getVerticalGridView();
+        final VerticalGridView verticalGridView = detailsFragment.getRowsFragment()
+                .getVerticalGridView();
         final int windowHeight = verticalGridView.getHeight();
         final int windowWidth = verticalGridView.getWidth();
         // make sure background manager attached to window is same size as VerticalGridView
@@ -101,7 +179,7 @@
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
-                return bitmapDrawable.getVerticalOffset() == mDefaultVerticalOffset
+                return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
                         && detailsFragment.getView()
                         .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
             }
@@ -119,4 +197,192 @@
         assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
         assertEquals(windowHeight, colorDrawable.getBounds().bottom);
     }
+
+    public static class DetailsFragmentWithVideo extends DetailsTestFragment {
+
+        final DetailsFragmentBackgroundController mDetailsBackground =
+                new DetailsFragmentBackgroundController(this);
+        MediaPlayerGlue mGlue;
+
+        public DetailsFragmentWithVideo() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mGlue = new MediaPlayerGlue(getActivity());
+            mDetailsBackground.setupVideoPlayback(mGlue);
+
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("A Googleer");
+            mGlue.setTitle("Diving with Sharks");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            setItem(new PhotoItem("Hello world", "Fake content goes here",
+                    android.support.v17.leanback.test.R.drawable.spiderman));
+            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 navigateBetweenRowsAndVideoUsingRequestFocus() throws Throwable {
+        launchAndWaitActivity(DetailsFragmentWithVideo.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        final DetailsFragmentWithVideo detailsFragment =
+                (DetailsFragmentWithVideo) mActivity.getTestFragment();
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoFragment != null
+                        && detailsFragment.mVideoFragment.getView() != null
+                        && detailsFragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+                .getHeight();
+        final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        assertTrue(firstRow.hasFocus());
+        assertTrue(firstRow.getTop() < screenHeight);
+        assertTrue(detailsFragment.isShowingTitle());
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.mVideoFragment.getView().requestFocus();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() >= screenHeight;
+            }
+        });
+        assertFalse(detailsFragment.isShowingTitle());
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.getRowsFragment().getVerticalGridView().requestFocus();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() == originalFirstRowTop;
+            }
+        });
+        assertTrue(detailsFragment.isShowingTitle());
+    }
+
+    @Test
+    public void navigateBetweenRowsAndVideoUsingDPAD() throws Throwable {
+        launchAndWaitActivity(DetailsFragmentWithVideo.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        final DetailsFragmentWithVideo detailsFragment =
+                (DetailsFragmentWithVideo) mActivity.getTestFragment();
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoFragment != null
+                        && detailsFragment.mVideoFragment.getView() != null
+                        && detailsFragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+                .getHeight();
+        final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        assertTrue(firstRow.hasFocus());
+        assertTrue(firstRow.getTop() < screenHeight);
+        assertTrue(detailsFragment.isShowingTitle());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() >= screenHeight;
+            }
+        });
+        assertFalse(detailsFragment.isShowingTitle());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() == originalFirstRowTop;
+            }
+        });
+        assertTrue(detailsFragment.isShowingTitle());
+    }
+
+    @Test
+    public void navigateBetweenRowsAndTitle() throws Throwable {
+        launchAndWaitActivity(DetailsTestFragment.class, new Options().uiVisibility(
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsTestFragment detailsFragment =
+                (DetailsTestFragment) mActivity.getTestFragment();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                    }
+                });
+                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.getRowsFragment().getVerticalGridView().getChildCount() > 0;
+            }
+        });
+        final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+                .getHeight();
+
+        assertTrue(firstRow.hasFocus());
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(firstRow.getTop() < screenHeight);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(detailsFragment.getTitleView().hasFocus());
+        assertEquals(originalFirstRowTop, firstRow.getTop());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(firstRow.hasFocus());
+        assertEquals(originalFirstRowTop, firstRow.getTop());
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java
deleted file mode 100644
index 9225ade..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-import android.view.View;
-
-/**
- * Activity containing {@link DetailsFragmentTest} used for testing.
- */
-public class DetailsFragmentTestActivity extends Activity {
-    private DetailsTestFragment mFragment;
-
-    /**
-     * Called when the activity is first created.
-     */
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
-        getWindow().getDecorView().setSystemUiVisibility(uiOptions);
-        setContentView(R.layout.details);
-        mFragment = new DetailsTestFragment();
-
-        if (savedInstanceState == null) {
-            Intent intent = getIntent();
-            if (intent.getExtras() != null) {
-                Bundle arguments = new Bundle();
-                arguments.putAll(intent.getExtras());
-                mFragment.setArguments(arguments);
-            }
-            FragmentTransaction ft = getFragmentManager().beginTransaction();
-            ft.replace(R.id.fragment_root, mFragment);
-            ft.commit();
-        }
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        mFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
-                R.drawable.spiderman));
-    }
-
-    public DetailsTestFragment getDetailsFragment() {
-        return mFragment;
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsParallaxManagerTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsParallaxManagerTest.java
deleted file mode 100644
index 6e1c22c..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsParallaxManagerTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v17.leanback.widget.ParallaxRecyclerViewSource;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link DetailsParallaxManager}.
- */
-@RunWith(JUnit4.class)
-@SmallTest
-public class DetailsParallaxManagerTest {
-
-    @Rule
-    public ActivityTestRule<DetailsFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(DetailsFragmentTestActivity.class);
-    private DetailsFragmentTestActivity mActivity;
-
-    @Before
-    public void setUp() {
-        mActivity = activityTestRule.getActivity();
-    }
-
-    @Test
-    public void setupTest() {
-        double delta = 0.0002;
-        DetailsParallaxManager dpm = new DetailsParallaxManager();
-        dpm.setRecyclerView(mActivity.getDetailsFragment().getRowsFragment().getVerticalGridView());
-
-        assertNotNull(dpm.getParallax());
-
-        ParallaxRecyclerViewSource.ChildPositionProperty frameTop = dpm.getFrameTop();
-        assertEquals(0f, frameTop.getFraction(), delta);
-        assertEquals(0f, frameTop.getAdapterPosition(), delta);
-
-
-        ParallaxRecyclerViewSource.ChildPositionProperty frameBottom = dpm.getFrameBottom();
-        assertEquals(1f, frameBottom.getFraction(), delta);
-        assertEquals(0f, frameBottom.getAdapterPosition(), delta);
-    }
-}
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
new file mode 100644
index 0000000..3880a7c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -0,0 +1,391 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static junit.framework.TestCase.assertFalse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.animation.PropertyValuesHolder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.v17.leanback.R;
+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.widget.DetailsParallax;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+import android.support.v17.leanback.widget.RecyclerViewParallax;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link DetailsSupportFragment}.
+ */
+@RunWith(JUnit4.class)
+@MediumTest
+public class DetailsSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    static final int PARALLAX_VERTICAL_OFFSET = -300;
+
+    public static class DetailsSupportFragmentParallax extends DetailsTestSupportFragment {
+
+        private DetailsParallaxDrawable mParallaxDrawable;
+
+        public DetailsSupportFragmentParallax() {
+            super();
+            mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            Drawable coverDrawable = new FitWidthBitmapDrawable();
+            mParallaxDrawable = new DetailsParallaxDrawable(
+                    getActivity(),
+                    getParallax(),
+                    coverDrawable,
+                    new ParallaxTarget.PropertyValuesHolderTarget(
+                            coverDrawable,
+                            PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
+                    )
+            );
+
+            BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
+            backgroundManager.attach(getActivity().getWindow());
+            backgroundManager.setDrawable(mParallaxDrawable);
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            setItem(new PhotoItem("Hello world", "Fake content goes here",
+                    android.support.v17.leanback.test.R.drawable.spiderman));
+        }
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+                    android.support.v17.leanback.test.R.drawable.spiderman);
+            ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
+        }
+
+        DetailsParallaxDrawable getParallaxDrawable() {
+            return mParallaxDrawable;
+        }
+    }
+
+    @Test
+    public void parallaxSetupTest() {
+        launchAndWaitActivity(DetailsSupportFragmentTest.DetailsSupportFragmentParallax.class,
+                new SingleSupportFragmentTestBase.Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        double delta = 0.0002;
+        DetailsParallax dpm = ((DetailsSupportFragment) mActivity.getTestFragment()).getParallax();
+
+        RecyclerViewParallax.ChildPositionProperty frameTop =
+                (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
+        assertEquals(0f, frameTop.getFraction(), delta);
+        assertEquals(0f, frameTop.getAdapterPosition(), delta);
+
+
+        RecyclerViewParallax.ChildPositionProperty frameBottom =
+                (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
+        assertEquals(1f, frameBottom.getFraction(), delta);
+        assertEquals(0f, frameBottom.getAdapterPosition(), delta);
+    }
+
+    @Test
+    public void parallaxTest() throws Throwable {
+        launchAndWaitActivity(DetailsSupportFragmentParallax.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        final DetailsSupportFragmentParallax detailsFragment =
+                (DetailsSupportFragmentParallax) mActivity.getTestFragment();
+        DetailsParallaxDrawable drawable =
+                detailsFragment.getParallaxDrawable();
+        final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
+                drawable.getCoverDrawable();
+
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.getRowsSupportFragment().getAdapter() != null
+                        && detailsFragment.getRowsSupportFragment().getAdapter().size() > 1;
+            }
+        });
+
+        final VerticalGridView verticalGridView = detailsFragment.getRowsSupportFragment()
+                .getVerticalGridView();
+        final int windowHeight = verticalGridView.getHeight();
+        final int windowWidth = verticalGridView.getWidth();
+        // make sure background manager attached to window is same size as VerticalGridView
+        // i.e. no status bar.
+        assertEquals(windowHeight, mActivity.getWindow().getDecorView().getHeight());
+        assertEquals(windowWidth, mActivity.getWindow().getDecorView().getWidth());
+
+        final View detailsFrame = verticalGridView.findViewById(R.id.details_frame);
+
+        assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+        final Rect detailsFrameRect = new Rect();
+        detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+        verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+        assertEquals(Math.min(windowHeight, detailsFrameRect.top),
+                bitmapDrawable.getBounds().height());
+        assertEquals(0, bitmapDrawable.getVerticalOffset());
+
+        assertTrue("TitleView is visible", detailsFragment.getView()
+                .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
+
+        activityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                verticalGridView.scrollToPosition(1);
+            }
+        });
+
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
+                        && detailsFragment.getView()
+                        .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
+            }
+        });
+
+        detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+        verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+        assertEquals(0, bitmapDrawable.getBounds().top);
+        assertEquals(Math.max(detailsFrameRect.top, 0), bitmapDrawable.getBounds().bottom);
+        assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+        ColorDrawable colorDrawable = (ColorDrawable) (drawable.getChildAt(1).getDrawable());
+        assertEquals(windowWidth, colorDrawable.getBounds().width());
+        assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
+        assertEquals(windowHeight, colorDrawable.getBounds().bottom);
+    }
+
+    public static class DetailsSupportFragmentWithVideo extends DetailsTestSupportFragment {
+
+        final DetailsSupportFragmentBackgroundController mDetailsBackground =
+                new DetailsSupportFragmentBackgroundController(this);
+        MediaPlayerGlue mGlue;
+
+        public DetailsSupportFragmentWithVideo() {
+            mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mDetailsBackground.enableParallax();
+            mGlue = new MediaPlayerGlue(getActivity());
+            mDetailsBackground.setupVideoPlayback(mGlue);
+
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("A Googleer");
+            mGlue.setTitle("Diving with Sharks");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            setItem(new PhotoItem("Hello world", "Fake content goes here",
+                    android.support.v17.leanback.test.R.drawable.spiderman));
+            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 navigateBetweenRowsAndVideoUsingRequestFocus() throws Throwable {
+        launchAndWaitActivity(DetailsSupportFragmentWithVideo.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        final DetailsSupportFragmentWithVideo detailsFragment =
+                (DetailsSupportFragmentWithVideo) mActivity.getTestFragment();
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoSupportFragment != null
+                        && detailsFragment.mVideoSupportFragment.getView() != null
+                        && detailsFragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+                .getHeight();
+        final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        assertTrue(firstRow.hasFocus());
+        assertTrue(firstRow.getTop() < screenHeight);
+        assertTrue(detailsFragment.isShowingTitle());
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.mVideoSupportFragment.getView().requestFocus();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() >= screenHeight;
+            }
+        });
+        assertFalse(detailsFragment.isShowingTitle());
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.getRowsSupportFragment().getVerticalGridView().requestFocus();
+            }
+        });
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() == originalFirstRowTop;
+            }
+        });
+        assertTrue(detailsFragment.isShowingTitle());
+    }
+
+    @Test
+    public void navigateBetweenRowsAndVideoUsingDPAD() throws Throwable {
+        launchAndWaitActivity(DetailsSupportFragmentWithVideo.class,
+                new Options().uiVisibility(
+                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+        final DetailsSupportFragmentWithVideo detailsFragment =
+                (DetailsSupportFragmentWithVideo) mActivity.getTestFragment();
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return detailsFragment.mVideoSupportFragment != null
+                        && detailsFragment.mVideoSupportFragment.getView() != null
+                        && detailsFragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+                .getHeight();
+        final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        assertTrue(firstRow.hasFocus());
+        assertTrue(firstRow.getTop() < screenHeight);
+        assertTrue(detailsFragment.isShowingTitle());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() >= screenHeight;
+            }
+        });
+        assertFalse(detailsFragment.isShowingTitle());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return firstRow.getTop() == originalFirstRowTop;
+            }
+        });
+        assertTrue(detailsFragment.isShowingTitle());
+    }
+
+    @Test
+    public void navigateBetweenRowsAndTitle() throws Throwable {
+        launchAndWaitActivity(DetailsTestSupportFragment.class, new Options().uiVisibility(
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+        final DetailsTestSupportFragment detailsFragment =
+                (DetailsTestSupportFragment) mActivity.getTestFragment();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                    }
+                });
+                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.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
+            }
+        });
+        final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+        final int originalFirstRowTop = firstRow.getTop();
+        final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+                .getHeight();
+
+        assertTrue(firstRow.hasFocus());
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(firstRow.getTop() < screenHeight);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(detailsFragment.getTitleView().hasFocus());
+        assertEquals(originalFirstRowTop, firstRow.getTop());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+        assertTrue(detailsFragment.isShowingTitle());
+        assertTrue(firstRow.hasFocus());
+        assertEquals(originalFirstRowTop, firstRow.getTop());
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
index bb78d30..ef6a1a3 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
@@ -16,8 +16,6 @@
 package android.support.v17.leanback.app;
 
 import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.v17.leanback.test.R;
@@ -27,7 +25,6 @@
 import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.support.v17.leanback.widget.DetailsOverviewRow;
 import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.FullWidthDetailsOverviewSharedElementHelper;
 import android.support.v17.leanback.widget.HeaderItem;
 import android.support.v17.leanback.widget.ImageCardView;
 import android.support.v17.leanback.widget.ListRow;
@@ -36,10 +33,10 @@
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import android.view.ViewGroup;
 
+/**
+ * Base class provides overview row and some related rows.
+ */
 public class DetailsTestFragment extends android.support.v17.leanback.app.DetailsFragment {
-    private static final String ITEM = "item";
-    public static final String VERTICAL_OFFSET = "details_fragment";
-
     private static final int NUM_ROWS = 3;
     private ArrayObjectAdapter mRowsAdapter;
     private PhotoItem mPhotoItem;
@@ -65,39 +62,22 @@
         }
     };
 
-    private static final int ACTION_PLAY = 1;
     private static final int ACTION_RENT = 2;
     private static final int ACTION_BUY = 3;
 
-    private static final long TIME_TO_LOAD_OVERVIEW_ROW_MS = 1000;
-    private static final long TIME_TO_LOAD_RELATED_ROWS_MS = 2000;
+    protected long mTimeToLoadOverviewRow = 1000;
+    protected long mTimeToLoadRelatedRow = 2000;
 
-    private Action mActionPlay;
     private Action mActionRent;
     private Action mActionBuy;
 
-    private FullWidthDetailsOverviewSharedElementHelper mHelper;
-    private DetailsBackgroundParallaxHelper mParallaxHelper;
-    private int mMinVerticalOffset;
+    protected int mMinVerticalOffset = -100;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setTitle("Leanback Sample App");
 
-        if (getArguments() != null) {
-            mMinVerticalOffset = getArguments().getInt(VERTICAL_OFFSET, -100);
-        }
-        mParallaxHelper = new DetailsBackgroundParallaxHelper.ParallaxBuilder(
-                getActivity(),
-                getParallaxManager())
-                .setCoverImageMinVerticalOffset(mMinVerticalOffset)
-                .build();
-        BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
-        backgroundManager.attach(getActivity().getWindow());
-        backgroundManager.setDrawable(mParallaxHelper.getDrawable());
-
-        mActionPlay = new Action(ACTION_PLAY, "Play");
         mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
                 getResources().getDrawable(R.drawable.ic_action_a));
         mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
@@ -119,12 +99,6 @@
         mRowsAdapter = new ArrayObjectAdapter(ps);
     }
 
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putParcelable(ITEM, mPhotoItem);
-    }
-
     public void setItem(PhotoItem photoItem) {
         mPhotoItem = photoItem;
         mRowsAdapter.clear();
@@ -143,7 +117,7 @@
                 mRowsAdapter.add(0, dor);
                 setSelectedPosition(0, true);
             }
-        }, TIME_TO_LOAD_OVERVIEW_ROW_MS);
+        }, mTimeToLoadOverviewRow);
 
 
         new Handler().postDelayed(new Runnable() {
@@ -161,20 +135,9 @@
                     mRowsAdapter.add(new ListRow(header, listRowAdapter));
                 }
             }
-        }, TIME_TO_LOAD_RELATED_ROWS_MS);
+        }, mTimeToLoadRelatedRow);
 
         setAdapter(mRowsAdapter);
     }
 
-    @Override
-    public void onResume() {
-        super.onResume();
-        Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
-                R.drawable.spiderman);
-        mParallaxHelper.setCoverImageBitmap(bitmap);
-    }
-
-    DetailsBackgroundParallaxHelper getParallaxHelper() {
-        return mParallaxHelper;
-    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
new file mode 100644
index 0000000..d963c3e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
@@ -0,0 +1,146 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsTestFragment.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.ViewGroup;
+
+/**
+ * Base class provides overview row and some related rows.
+ */
+public class DetailsTestSupportFragment extends android.support.v17.leanback.app.DetailsSupportFragment {
+    private static final int NUM_ROWS = 3;
+    private ArrayObjectAdapter mRowsAdapter;
+    private PhotoItem mPhotoItem;
+    private final Presenter mCardPresenter = new Presenter() {
+        @Override
+        public ViewHolder onCreateViewHolder(ViewGroup parent) {
+            ImageCardView cardView = new ImageCardView(getActivity());
+            cardView.setFocusable(true);
+            cardView.setFocusableInTouchMode(true);
+            return new ViewHolder(cardView);
+        }
+
+        @Override
+        public void onBindViewHolder(ViewHolder viewHolder, Object item) {
+            ImageCardView imageCardView = (ImageCardView) viewHolder.view;
+            imageCardView.setTitleText("Android Tv");
+            imageCardView.setContentText("Android Tv Production Inc.");
+            imageCardView.setMainImageDimensions(313, 176);
+        }
+
+        @Override
+        public void onUnbindViewHolder(ViewHolder viewHolder) {
+        }
+    };
+
+    private static final int ACTION_RENT = 2;
+    private static final int ACTION_BUY = 3;
+
+    protected long mTimeToLoadOverviewRow = 1000;
+    protected long mTimeToLoadRelatedRow = 2000;
+
+    private Action mActionRent;
+    private Action mActionBuy;
+
+    protected int mMinVerticalOffset = -100;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setTitle("Leanback Sample App");
+
+        mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
+                getResources().getDrawable(R.drawable.ic_action_a));
+        mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
+
+        ClassPresenterSelector ps = new ClassPresenterSelector();
+        FullWidthDetailsOverviewRowPresenter dorPresenter =
+                new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
+                    @Override
+                    protected void onBindDescription(
+                            AbstractDetailsDescriptionPresenter.ViewHolder vh, Object item) {
+                        vh.getTitle().setText("Funny Movie");
+                        vh.getSubtitle().setText("Android TV Production Inc.");
+                        vh.getBody().setText("What a great movie!");
+                    }
+                });
+
+        ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
+        ps.addClassPresenter(ListRow.class, new ListRowPresenter());
+        mRowsAdapter = new ArrayObjectAdapter(ps);
+    }
+
+    public void setItem(PhotoItem photoItem) {
+        mPhotoItem = photoItem;
+        mRowsAdapter.clear();
+        new Handler().postDelayed(new Runnable() {
+            public void run() {
+                if (getActivity() == null) {
+                    return;
+                }
+                Resources res = getActivity().getResources();
+                DetailsOverviewRow dor = new DetailsOverviewRow(mPhotoItem.getTitle());
+                dor.setImageDrawable(res.getDrawable(mPhotoItem.getImageResourceId()));
+                SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();
+                adapter.set(ACTION_RENT, mActionRent);
+                adapter.set(ACTION_BUY, mActionBuy);
+                dor.setActionsAdapter(adapter);
+                mRowsAdapter.add(0, dor);
+                setSelectedPosition(0, true);
+            }
+        }, mTimeToLoadOverviewRow);
+
+
+        new Handler().postDelayed(new Runnable() {
+            public void run() {
+                if (getActivity() == null) {
+                    return;
+                }
+                for (int i = 0; i < NUM_ROWS; ++i) {
+                    ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(mCardPresenter);
+                    listRowAdapter.add(new PhotoItem("Hello world", R.drawable.spiderman));
+                    listRowAdapter.add(new PhotoItem("This is a test", R.drawable.spiderman));
+                    listRowAdapter.add(new PhotoItem("Android TV", R.drawable.spiderman));
+                    listRowAdapter.add(new PhotoItem("Leanback", R.drawable.spiderman));
+                    HeaderItem header = new HeaderItem(i, "Row " + i);
+                    mRowsAdapter.add(new ListRow(header, listRowAdapter));
+                }
+            }
+        }, mTimeToLoadRelatedRow);
+
+        setAdapter(mRowsAdapter);
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
new file mode 100644
index 0000000..7b64e0b
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.FocusHighlightHelper;
+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.VerticalGridView;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HeadersFragmentTest extends SingleFragmentTestBase {
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public static class F_defaultScale extends HeadersFragment {
+        final ListRowPresenter mPresenter = new ListRowPresenter();
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
+            setAdapter(adapter);
+            loadData(adapter, 10);
+        }
+    }
+
+    @Test
+    public void defaultScale() {
+        launchAndWaitActivity(F_defaultScale.class, 1000);
+
+        final VerticalGridView gridView = ((HeadersFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(0);
+        assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
+        assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
+    }
+
+    public static class F_disableScale extends HeadersFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+            setAdapter(adapter);
+            loadData(adapter, 10);
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
+        }
+    }
+
+    @Test
+    public void disableScale() {
+        launchAndWaitActivity(F_disableScale.class, 1000);
+
+        final VerticalGridView gridView = ((HeadersFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(0);
+        assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+        assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
new file mode 100644
index 0000000..f545572
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
@@ -0,0 +1,103 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from HeadersFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.FocusHighlightHelper;
+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.VerticalGridView;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HeadersSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public static class F_defaultScale extends HeadersSupportFragment {
+        final ListRowPresenter mPresenter = new ListRowPresenter();
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
+            setAdapter(adapter);
+            loadData(adapter, 10);
+        }
+    }
+
+    @Test
+    public void defaultScale() {
+        launchAndWaitActivity(F_defaultScale.class, 1000);
+
+        final VerticalGridView gridView = ((HeadersSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(0);
+        assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
+        assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
+    }
+
+    public static class F_disableScale extends HeadersSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+            setAdapter(adapter);
+            loadData(adapter, 10);
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
+        }
+    }
+
+    @Test
+    public void disableScale() {
+        launchAndWaitActivity(F_disableScale.class, 1000);
+
+        final VerticalGridView gridView = ((HeadersSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(0);
+        assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+        assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
index b7cb4e8..0b40920 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
@@ -197,6 +197,48 @@
     }
 
     @Test
+    public void adapterSize_rowsRemoveAll() {
+        ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);
+        adapter.add(new SectionRow("section 1"));
+        for (int i = 0; i < 4; i++) {
+            HeaderItem headerItem = new HeaderItem(i, "header "+i);
+            adapter.add(new ListRow(headerItem, createListRowAdapter()));
+        }
+
+        ListRowDataAdapter listRowDataAdapter = new ListRowDataAdapter(adapter);
+        assertEquals(5, listRowDataAdapter.size());
+
+        adapter.clear();
+        assertEquals(0, listRowDataAdapter.size());
+
+        HeaderItem headerItem = new HeaderItem(10, "header "+10);
+        adapter.add(new ListRow(headerItem, createListRowAdapter()));
+        assertEquals(1, listRowDataAdapter.size());
+    }
+
+    @Test
+    public void changeRemove_revealInvisibleItems() {
+        ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);
+        for (int i = 0; i < 4; i++) {
+            HeaderItem headerItem = new HeaderItem(i, "header "+i);
+            adapter.add(new ListRow(headerItem, createListRowAdapter()));
+        }
+        adapter.add(new SectionRow("section"));
+        for (int i = 4; i < 8; i++) {
+            HeaderItem headerItem = new HeaderItem(i, "header "+i);
+            adapter.add(new ListRow(headerItem, createListRowAdapter()));
+        }
+
+        ListRowDataAdapter listRowDataAdapter = new ListRowDataAdapter(adapter);
+        assertEquals(9, listRowDataAdapter.size());
+
+        listRowDataAdapter.registerObserver(dataObserver);
+        adapter.removeItems(5, 4);
+        verify(dataObserver, times(1)).onItemRangeRemoved(4, 5);
+        assertEquals(4, listRowDataAdapter.size());
+    }
+
+    @Test
     public void adapterSize_rowsRemoved() {
         int itemCount = 4;
         ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
index 748a39fd..944c1f7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
@@ -51,7 +51,7 @@
 public class PlaybackControlSupportGlueTest {
 
 
-    public  static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
+    static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
         int mSpeedId = PLAYBACK_SPEED_PAUSED;
         // number of times onRowChanged callback is called
         int mOnRowChangedCallCount = 0;
@@ -65,7 +65,7 @@
         }
 
         PlayControlGlueImpl(Context context, PlaybackOverlaySupportFragment fragment,
-                int[] seekSpeeds) {
+                                   int[] seekSpeeds) {
             super(context, fragment, seekSpeeds);
         }
 
@@ -534,15 +534,15 @@
     @Test
     public void testOnItemClickedListener() {
         PlaybackControlsRow row = new PlaybackControlsRow();
-        final PlaybackOverlayFragment[] fragmentResult = new PlaybackOverlayFragment[1];
+        final PlaybackOverlaySupportFragment[] fragmentResult = new PlaybackOverlaySupportFragment[1];
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                fragmentResult[0] = new PlaybackOverlayFragment();
+                fragmentResult[0] = new PlaybackOverlaySupportFragment();
             }
         });
-        PlaybackOverlayFragment fragment = fragmentResult[0];
-        glue.setHost(new PlaybackControlSupportGlue.PlaybackGlueHostOld(fragment));
+        PlaybackOverlaySupportFragment fragment = fragmentResult[0];
+        glue.setHost(new PlaybackControlSupportGlue.PlaybackSupportGlueHostOld(fragment));
         glue.setControlsRow(row);
         SparseArrayObjectAdapter adapter = (SparseArrayObjectAdapter)
                 row.getPrimaryActionsAdapter();
@@ -560,7 +560,7 @@
         // Initially media is paused
         assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_PAUSED, glue.getCurrentSpeedId());
 
-        // simulate a click inside PlaybackOverlayFragment's PlaybackRow.
+        // simulate a click inside PlaybackOverlaySupportFragment's PlaybackRow.
         fragment.getOnItemViewClickedListener().onItemClicked(vh, playPause, rowVh, row);
         verify(listener, times(0)).onItemClicked(vh, playPause, rowVh, row);
         assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_NORMAL, glue.getCurrentSpeedId());
@@ -605,5 +605,4 @@
         // notifyPlaybackRowChanged on the old host and finally onRowChanged on the glue.
         assertEquals(playbackGlue.getOnRowChangedCallCount(), 2);
     }
-
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
index bf7077b..2fcf3ed 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -15,18 +15,18 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +37,6 @@
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import android.view.KeyEvent;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -45,21 +44,35 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackFragmentTest {
+public class PlaybackFragmentTest extends SingleFragmentTestBase {
 
     private static final String TAG = "PlaybackFragmentTest";
     private static final long TRANSITION_LENGTH = 1000;
 
-    @Rule
-    public ActivityTestRule<PlaybackTestActivity> activityTestRule =
-            new ActivityTestRule<>(PlaybackTestActivity.class, false, false);
-    private PlaybackTestActivity mActivity;
+    @Test
+    public void testDetachCalledWhenDestroyFragment() throws Throwable {
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+        PlaybackGlue glue = fragment.getGlue();
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mActivity.finish();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mActivity.isDestroyed();
+            }
+        });
+        assertNull(glue.getHost());
+    }
 
     @Test
     public void testSelectedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -126,9 +139,9 @@
 
     @Test
     public void testClickedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -210,10 +223,4 @@
                 listRowItemPassed);
     }
 
-    private void sendKeys(int ...keys) {
-        for (int i = 0; i < keys.length; i++) {
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
-        }
-    }
-
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
index f27ace0..dfc3458 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
@@ -18,32 +18,26 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.test.R;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackOverlayFragmentTest {
-
-    @Rule
-    public ActivityTestRule<PlaybackOverlayTestActivity> activityTestRule =
-            new ActivityTestRule<>(PlaybackOverlayTestActivity.class, false, false);
-    private PlaybackOverlayTestActivity mActivity;
+public class PlaybackOverlayFragmentTest extends SingleFragmentTestBase {
 
     @Test
     public void workaroundVideoViewStealFocus() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(PlaybackOverlayTestFragment.class,
+                new Options().activityLayoutId(R.layout.playback_controls_with_video), 0);
+        PlaybackOverlayTestFragment fragment = (PlaybackOverlayTestFragment)
+                mActivity.getTestFragment();
 
         assertFalse(mActivity.findViewById(R.id.videoView).hasFocus());
-        assertTrue(mActivity.getPlaybackFragment().getView().hasFocus());
+        assertTrue(fragment.getView().hasFocus());
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
deleted file mode 100644
index ea2aa38..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackOverlayTestActivity extends Activity {
-    private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.playback_controls_with_video);
-    }
-
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
-        for (PictureInPictureListener listener : mListeners) {
-            listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
-        }
-    }
-
-    public void registerPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.remove(listener);
-    }
-
-    public interface PictureInPictureListener {
-        void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
-    }
-
-    public PlaybackOverlayTestFragment getPlaybackFragment() {
-        return (PlaybackOverlayTestFragment) getFragmentManager().findFragmentById(
-                R.id.playback_controls_fragment);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
index b44dd09..e5dbe08 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
@@ -40,9 +40,7 @@
 import android.view.View;
 import android.widget.Toast;
 
-public class PlaybackOverlayTestFragment
-        extends PlaybackOverlayFragment
-        implements PlaybackOverlayTestActivity.PictureInPictureListener {
+public class PlaybackOverlayTestFragment extends PlaybackOverlayFragment {
     private static final String TAG = "leanback.PlaybackControlsFragment";
 
     /**
@@ -169,26 +167,6 @@
     public void onStart() {
         super.onStart();
         mGlue.setFadingEnabled(true);
-        mGlue.enableProgressUpdating(mGlue.hasValidMedia() && mGlue.isMediaPlaying());
-        ((PlaybackOverlayTestActivity) getActivity()).registerPictureInPictureListener(this);
-    }
-
-    @Override
-    public void onStop() {
-        mGlue.enableProgressUpdating(false);
-        ((PlaybackOverlayTestActivity) getActivity()).unregisterPictureInPictureListener(this);
-        super.onStop();
-    }
-
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
-        if (isInPictureInPictureMode) {
-            // Hide the controls in picture-in-picture mode.
-            setFadingEnabled(true);
-            fadeOut();
-        } else {
-            setFadingEnabled(mGlue.isMediaPlaying());
-        }
     }
 
     abstract static class PlaybackControlHelper extends PlaybackControlGlue {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
index fdba125..d33e3ef 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -15,18 +18,18 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +40,6 @@
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import android.view.KeyEvent;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -45,21 +47,35 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackSupportFragmentTest {
+public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase {
 
     private static final String TAG = "PlaybackSupportFragmentTest";
     private static final long TRANSITION_LENGTH = 1000;
 
-    @Rule
-    public ActivityTestRule<PlaybackSupportTestActivity> activityTestRule =
-            new ActivityTestRule<>(PlaybackSupportTestActivity.class, false, false);
-    private PlaybackSupportTestActivity mActivity;
+    @Test
+    public void testDetachCalledWhenDestroyFragment() throws Throwable {
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+        PlaybackGlue glue = fragment.getGlue();
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mActivity.finish();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mActivity.isDestroyed();
+            }
+        });
+        assertNull(glue.getHost());
+    }
 
     @Test
     public void testSelectedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -82,8 +98,7 @@
 
         ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
                 ArgumentCaptor.forClass(Presenter.ViewHolder.class);
-        ArgumentCaptor<Object> itemCaptor =
-                ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
         ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
                 ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
         ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
@@ -127,9 +142,9 @@
 
     @Test
     public void testClickedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -211,10 +226,4 @@
                 listRowItemPassed);
     }
 
-    private void sendKeys(int ...keys) {
-        for (int i = 0; i < keys.length; i++) {
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
-        }
-    }
-
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
deleted file mode 100644
index c85fe83..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-import android.support.v4.app.FragmentActivity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackSupportTestActivity extends FragmentActivity {
-    private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.playback_support_controls);
-    }
-
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
-        for (PictureInPictureListener listener : mListeners) {
-            listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
-        }
-    }
-
-    public void registerPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.remove(listener);
-    }
-
-    public interface PictureInPictureListener {
-        void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
-    }
-
-    public PlaybackSupportTestFragment getPlaybackFragment() {
-        return (PlaybackSupportTestFragment) getSupportFragmentManager().findFragmentById(
-                R.id.playback_controls_fragment);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
deleted file mode 100644
index ff840ec..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackTestActivity extends Activity {
-    private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.playback_controls);
-    }
-
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
-        for (PictureInPictureListener listener : mListeners) {
-            listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
-        }
-    }
-
-    public void registerPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
-        mListeners.remove(listener);
-    }
-
-    public interface PictureInPictureListener {
-        void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
-    }
-
-    public PlaybackTestFragment getPlaybackFragment() {
-        return (PlaybackTestFragment) getFragmentManager().findFragmentById(
-                R.id.playback_controls_fragment);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
index 043d73e..f9fd33f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -19,7 +19,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
-
 import android.support.v17.leanback.media.PlaybackControlGlue;
 import android.support.v17.leanback.test.R;
 import android.support.v17.leanback.widget.Action;
@@ -28,7 +27,6 @@
 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.OnItemViewSelectedListener;
 import android.support.v17.leanback.widget.PlaybackControlsRow;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.PresenterSelector;
@@ -75,22 +73,12 @@
         }
     };
 
-    private OnItemViewSelectedListener mOnItemViewSelectedListener =
-            new OnItemViewSelectedListener() {
-                @Override
-                public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
-                                           RowPresenter.ViewHolder rowViewHolder, Row row) {
-                    Log.d(TAG, "onItemSelected: " + item + " row " + row);
-                }
-            };
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         Log.i(TAG, "onCreate");
         super.onCreate(savedInstanceState);
 
         setBackgroundType(BACKGROUND_TYPE);
-       // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
 
         createComponents(getActivity());
         setOnItemViewClickedListener(mOnItemViewClickedListener);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
rename to v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
index 4a07a60..0973fe1 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackTestFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -19,7 +22,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
-
 import android.support.v17.leanback.media.PlaybackControlGlue;
 import android.support.v17.leanback.test.R;
 import android.support.v17.leanback.widget.Action;
@@ -28,7 +30,6 @@
 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.OnItemViewSelectedListener;
 import android.support.v17.leanback.widget.PlaybackControlsRow;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.PresenterSelector;
@@ -40,13 +41,13 @@
 import android.view.View;
 import android.widget.Toast;
 
-public class PlaybackSupportTestFragment extends PlaybackSupportFragment {
-    private static final String TAG = "PlaybackTestFragment";
+public class PlaybackTestSupportFragment extends PlaybackSupportFragment {
+    private static final String TAG = "PlaybackTestSupportFragment";
 
     /**
      * Change this to choose a different overlay background.
      */
-    private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+    private static final int BACKGROUND_TYPE = PlaybackSupportFragment.BG_LIGHT;
 
     private static final int ROW_CONTROLS = 0;
 
@@ -75,22 +76,12 @@
         }
     };
 
-    private OnItemViewSelectedListener mOnItemViewSelectedListener =
-            new OnItemViewSelectedListener() {
-                @Override
-                public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
-                                           RowPresenter.ViewHolder rowViewHolder, Row row) {
-                    Log.d(TAG, "onItemSelected: " + item + " row " + row);
-                }
-            };
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         Log.i(TAG, "onCreate");
         super.onCreate(savedInstanceState);
 
         setBackgroundType(BACKGROUND_TYPE);
-        // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
 
         createComponents(getActivity());
         setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -124,7 +115,7 @@
         };
 
         mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
-        //  mGlue.setOnI
+       //  mGlue.setOnI
         mListRowPresenter = new ListRowPresenter();
 
         setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
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 50d5f24..193203e 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
@@ -15,70 +15,75 @@
  */
 package android.support.v17.leanback.app;
 
-import android.content.Intent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
 import android.os.SystemClock;
-import android.support.test.filters.MediumTest;
 import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
+import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 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.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.View;
 
-import org.junit.After;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import java.lang.ref.WeakReference;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class RowsFragmentTest {
+public class RowsFragmentTest extends SingleFragmentTestBase {
 
-    static final long ACTIVITY_LOAD_DELAY = 2000;
+    static final StringPresenter sCardPresenter = new StringPresenter();
 
-    @Rule
-    public ActivityTestRule<RowsFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(RowsFragmentTestActivity.class, false, false);
-    private RowsFragmentTestActivity mActivity;
-
-    @After
-    public void afterTest() throws Throwable {
-        activityTestRule.runOnUiThread(new Runnable() {
-            public void run() {
-                if (mActivity != null) {
-                    mActivity.finish();
-                    mActivity = null;
-                }
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepFragment-" + (index++));
             }
-        });
-    }
-
-    private void sendKeys(int ...keys) {
-        for (int i = 0; i < keys.length; i++) {
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
         }
     }
 
-    void launchAndWaitActivity(Intent intent) {
-        mActivity = activityTestRule.launchActivity(intent);
-        SystemClock.sleep(ACTIVITY_LOAD_DELAY);
+    public static class F_defaultAlignment extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
     }
 
     @Test
     public void defaultAlignment() throws InterruptedException {
-        Intent intent = new Intent();
-        intent.putExtra(RowsFragmentTestActivity.EXTRA_NUM_ROWS, 10);
-        intent.putExtra(RowsFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, 1l);
-        launchAndWaitActivity(intent);
+        launchAndWaitActivity(F_defaultAlignment.class, 1000);
 
         final Rect rect = new Rect();
 
-        final VerticalGridView gridView = mActivity.getRowsTestFragment().getVerticalGridView();
+        final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
         View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
         rect.set(0, 0, row0.getWidth(), row0.getHeight());
         gridView.offsetDescendantRectToMyCoords(row0, rect);
@@ -93,4 +98,138 @@
         assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
     }
 
+    public static class F_selectBeforeSetAdapter extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    ListRowPresenter lrp = new ListRowPresenter();
+                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+                    setAdapter(adapter);
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeSetAdapter() throws InterruptedException {
+        launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectBeforeAddData extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeAddData() throws InterruptedException {
+        launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectAfterAddData extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setSelectedPosition(7, false);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectAfterAddData() throws InterruptedException {
+        launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+    public static class F_restoreSelection extends RowsFragment {
+        public F_restoreSelection() {
+            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            if (savedInstanceState == null) {
+                setSelectedPosition(7, false);
+            }
+        }
+    }
+
+    @Test
+    public void restoreSelection() {
+        launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        mActivity.recreate();
+                    }
+                }
+        );
+        SystemClock.sleep(1000);
+
+        // mActivity is invalid after recreate(), a new Activity instance is created
+        // but we could get Fragment from static variable.
+        RowsFragment fragment = sLastF_restoreSelection.get();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java
deleted file mode 100644
index fe2dd25..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-public class RowsFragmentTestActivity extends Activity {
-
-    public static final String EXTRA_NUM_ROWS = "numRows";
-    public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
-    public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
-    public final static String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        Intent intent = getIntent();
-
-        setContentView(R.layout.rows);
-        if (savedInstanceState == null) {
-            RowsTestFragment fragment = new RowsTestFragment();
-            Bundle arguments = new Bundle();
-            if (intent.getExtras() != null) {
-                arguments.putAll(intent.getExtras());
-            }
-            fragment.setArguments(arguments);
-            FragmentTransaction ft = getFragmentManager().beginTransaction();
-            ft.replace(R.id.main_frame, fragment);
-            ft.commit();
-        }
-    }
-
-    public RowsTestFragment getRowsTestFragment() {
-        return (RowsTestFragment) getFragmentManager().findFragmentById(R.id.main_frame);
-    }
-}
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 c024b6c..70ddbac 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
@@ -18,70 +18,75 @@
  */
 package android.support.v17.leanback.app;
 
-import android.content.Intent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
 import android.os.SystemClock;
-import android.support.test.filters.MediumTest;
 import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
+import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 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.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.View;
 
-import org.junit.After;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import java.lang.ref.WeakReference;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class RowsSupportFragmentTest {
+public class RowsSupportFragmentTest extends SingleSupportFragmentTestBase {
 
-    static final long ACTIVITY_LOAD_DELAY = 2000;
+    static final StringPresenter sCardPresenter = new StringPresenter();
 
-    @Rule
-    public ActivityTestRule<RowsSupportFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(RowsSupportFragmentTestActivity.class, false, false);
-    private RowsSupportFragmentTestActivity mActivity;
-
-    @After
-    public void afterTest() throws Throwable {
-        activityTestRule.runOnUiThread(new Runnable() {
-            public void run() {
-                if (mActivity != null) {
-                    mActivity.finish();
-                    mActivity = null;
-                }
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
             }
-        });
-    }
-
-    private void sendKeys(int ...keys) {
-        for (int i = 0; i < keys.length; i++) {
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
         }
     }
 
-    void launchAndWaitActivity(Intent intent) {
-        mActivity = activityTestRule.launchActivity(intent);
-        SystemClock.sleep(ACTIVITY_LOAD_DELAY);
+    public static class F_defaultAlignment extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
     }
 
     @Test
     public void defaultAlignment() throws InterruptedException {
-        Intent intent = new Intent();
-        intent.putExtra(RowsSupportFragmentTestActivity.EXTRA_NUM_ROWS, 10);
-        intent.putExtra(RowsSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, 1l);
-        launchAndWaitActivity(intent);
+        launchAndWaitActivity(F_defaultAlignment.class, 1000);
 
         final Rect rect = new Rect();
 
-        final VerticalGridView gridView = mActivity.getRowsTestSupportFragment().getVerticalGridView();
+        final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
         View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
         rect.set(0, 0, row0.getWidth(), row0.getHeight());
         gridView.offsetDescendantRectToMyCoords(row0, rect);
@@ -96,4 +101,138 @@
         assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
     }
 
+    public static class F_selectBeforeSetAdapter extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    ListRowPresenter lrp = new ListRowPresenter();
+                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+                    setAdapter(adapter);
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeSetAdapter() throws InterruptedException {
+        launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectBeforeAddData extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeAddData() throws InterruptedException {
+        launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectAfterAddData extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setSelectedPosition(7, false);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectAfterAddData() throws InterruptedException {
+        launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+    public static class F_restoreSelection extends RowsSupportFragment {
+        public F_restoreSelection() {
+            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            if (savedInstanceState == null) {
+                setSelectedPosition(7, false);
+            }
+        }
+    }
+
+    @Test
+    public void restoreSelection() {
+        launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        mActivity.recreate();
+                    }
+                }
+        );
+        SystemClock.sleep(1000);
+
+        // mActivity is invalid after recreate(), a new Activity instance is created
+        // but we could get Fragment from static variable.
+        RowsSupportFragment fragment = sLastF_restoreSelection.get();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java
deleted file mode 100644
index d736458..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from RowsFragmentTestActivity.java.  DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-public class RowsSupportFragmentTestActivity extends FragmentActivity {
-
-    public static final String EXTRA_NUM_ROWS = "numRows";
-    public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
-    public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
-    public final static String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        Intent intent = getIntent();
-
-        setContentView(R.layout.rows);
-        if (savedInstanceState == null) {
-            RowsTestSupportFragment fragment = new RowsTestSupportFragment();
-            Bundle arguments = new Bundle();
-            if (intent.getExtras() != null) {
-                arguments.putAll(intent.getExtras());
-            }
-            fragment.setArguments(arguments);
-            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
-            ft.replace(R.id.main_frame, fragment);
-            ft.commit();
-        }
-    }
-
-    public RowsTestSupportFragment getRowsTestSupportFragment() {
-        return (RowsTestSupportFragment) getSupportFragmentManager().findFragmentById(R.id.main_frame);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java
deleted file mode 100644
index d1f71db..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-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.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.util.Log;
-
-import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
-import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_NUM_ROWS;
-import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
-import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
-
-public class RowsTestFragment extends RowsFragment {
-    private static final String TAG = "RowsTestFragment";
-
-    final static int DEFAULT_NUM_ROWS = 100;
-    final static int DEFAULT_REPEAT_PER_ROW = 20;
-    final static long DEFAULT_LOAD_DATA_DELAY = 2000;
-    final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
-
-    private ArrayObjectAdapter mRowsAdapter;
-
-    // For good performance, it's important to use a single instance of
-    // a card presenter for all rows using that presenter.
-    final static StringPresenter sCardPresenter = new StringPresenter();
-
-    int NUM_ROWS;
-    int REPEAT_PER_ROW;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        Log.i(TAG, "onCreate");
-        super.onCreate(savedInstanceState);
-
-        Bundle arguments = getArguments();
-        NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, RowsTestFragment.DEFAULT_NUM_ROWS);
-        REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
-                DEFAULT_REPEAT_PER_ROW);
-        long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
-                DEFAULT_LOAD_DATA_DELAY);
-        final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
-                EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
-                DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
-
-        if (!SET_ADAPTER_AFTER_DATA_LOAD) {
-            setupRows();
-        }
-
-        setOnItemViewClickedListener(new ItemViewClickedListener());
-        setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
-            @Override
-            public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
-                    RowPresenter.ViewHolder rowViewHolder, Row row) {
-                Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
-                        + " " + rowViewHolder
-                        + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
-            }
-        });
-        // simulates in a real world use case  data being loaded two seconds later
-        new Handler().postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                if (getActivity() == null || getActivity().isDestroyed()) {
-                    return;
-                }
-                if (SET_ADAPTER_AFTER_DATA_LOAD) {
-                    setupRows();
-                }
-                loadData();
-            }
-        }, LOAD_DATA_DELAY);
-    }
-
-    private void setupRows() {
-        ListRowPresenter lrp = new ListRowPresenter();
-
-        mRowsAdapter = new ArrayObjectAdapter(lrp);
-
-        setAdapter(mRowsAdapter);
-    }
-
-    private void loadData() {
-        for (int i = 0; i < NUM_ROWS; ++i) {
-            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
-            int index = 0;
-            for (int j = 0; j < REPEAT_PER_ROW; ++j) {
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("This is a test-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("GuidedStepFragment-" + (index++));
-            }
-            HeaderItem header = new HeaderItem(i, "Row " + i);
-            mRowsAdapter.add(new ListRow(header, listRowAdapter));
-        }
-    }
-
-    private final class ItemViewClickedListener implements OnItemViewClickedListener {
-        @Override
-        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
-                RowPresenter.ViewHolder rowViewHolder, Row row) {
-            Log.i(TAG, "onItemClicked: " + item + " row " + row);
-        }
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java
deleted file mode 100644
index e095f94..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java
+++ /dev/null
@@ -1,132 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from RowsTestFragment.java.  DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-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.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.util.Log;
-
-import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
-import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_NUM_ROWS;
-import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
-import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
-
-public class RowsTestSupportFragment extends RowsSupportFragment {
-    private static final String TAG = "RowsTestSupportFragment";
-
-    final static int DEFAULT_NUM_ROWS = 100;
-    final static int DEFAULT_REPEAT_PER_ROW = 20;
-    final static long DEFAULT_LOAD_DATA_DELAY = 2000;
-    final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
-
-    private ArrayObjectAdapter mRowsAdapter;
-
-    // For good performance, it's important to use a single instance of
-    // a card presenter for all rows using that presenter.
-    final static StringPresenter sCardPresenter = new StringPresenter();
-
-    int NUM_ROWS;
-    int REPEAT_PER_ROW;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        Log.i(TAG, "onCreate");
-        super.onCreate(savedInstanceState);
-
-        Bundle arguments = getArguments();
-        NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, RowsTestSupportFragment.DEFAULT_NUM_ROWS);
-        REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
-                DEFAULT_REPEAT_PER_ROW);
-        long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
-                DEFAULT_LOAD_DATA_DELAY);
-        final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
-                EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
-                DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
-
-        if (!SET_ADAPTER_AFTER_DATA_LOAD) {
-            setupRows();
-        }
-
-        setOnItemViewClickedListener(new ItemViewClickedListener());
-        setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
-            @Override
-            public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
-                    RowPresenter.ViewHolder rowViewHolder, Row row) {
-                Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
-                        + " " + rowViewHolder
-                        + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
-            }
-        });
-        // simulates in a real world use case  data being loaded two seconds later
-        new Handler().postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                if (getActivity() == null || getActivity().isDestroyed()) {
-                    return;
-                }
-                if (SET_ADAPTER_AFTER_DATA_LOAD) {
-                    setupRows();
-                }
-                loadData();
-            }
-        }, LOAD_DATA_DELAY);
-    }
-
-    private void setupRows() {
-        ListRowPresenter lrp = new ListRowPresenter();
-
-        mRowsAdapter = new ArrayObjectAdapter(lrp);
-
-        setAdapter(mRowsAdapter);
-    }
-
-    private void loadData() {
-        for (int i = 0; i < NUM_ROWS; ++i) {
-            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
-            int index = 0;
-            for (int j = 0; j < REPEAT_PER_ROW; ++j) {
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("This is a test-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
-            }
-            HeaderItem header = new HeaderItem(i, "Row " + i);
-            mRowsAdapter.add(new ListRow(header, listRowAdapter));
-        }
-    }
-
-    private final class ItemViewClickedListener implements OnItemViewClickedListener {
-        @Override
-        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
-                RowPresenter.ViewHolder rowViewHolder, Row row) {
-            Log.i(TAG, "onItemClicked: " + item + " row " + row);
-        }
-    }
-}
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
new file mode 100644
index 0000000..d17811b
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+public class SingleFragmentTestActivity extends Activity {
+
+    /**
+     * Fragment that will be added to activity
+     */
+    public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+
+    public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
+    public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+
+        final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
+        if (uiOptions != 0) {
+            getWindow().getDecorView().setSystemUiVisibility(uiOptions);
+        }
+
+        setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+        if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
+            try {
+                Fragment fragment = (Fragment) Class.forName(
+                        intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+                FragmentTransaction ft = getFragmentManager().beginTransaction();
+                ft.replace(R.id.main_frame, fragment);
+                ft.commit();
+            } catch (Exception ex) {
+                ex.printStackTrace();
+                finish();
+            }
+        }
+    }
+
+    public Fragment getTestFragment() {
+        return getFragmentManager().findFragmentById(R.id.main_frame);
+    }
+}
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
new file mode 100644
index 0000000..fd1f922
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+public class SingleFragmentTestBase {
+
+    @Rule
+    public TestName mUnitTestName = new TestName();
+
+    @Rule
+    public ActivityTestRule<SingleFragmentTestActivity> activityTestRule =
+            new ActivityTestRule<>(SingleFragmentTestActivity.class, false, false);
+
+    protected SingleFragmentTestActivity mActivity;
+
+    @After
+    public void afterTest() throws Throwable {
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                if (mActivity != null) {
+                    mActivity.finish();
+                    mActivity = null;
+                }
+            }
+        });
+    }
+
+    public void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+
+    /**
+     * Options that will be passed throught Intent to SingleFragmentTestActivity
+     */
+    public static class Options {
+        int mActivityLayoutId;
+        int mUiVisibility;
+
+        public Options() {
+        }
+
+        public Options activityLayoutId(int activityLayoutId) {
+            mActivityLayoutId = activityLayoutId;
+            return this;
+        }
+
+        public Options uiVisibility(int uiVisibility) {
+            mUiVisibility = uiVisibility;
+            return this;
+        }
+
+        public void collect(Intent intent) {
+            if (mActivityLayoutId != 0) {
+                intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
+                        mActivityLayoutId);
+            }
+            if (mUiVisibility != 0) {
+                intent.putExtra(SingleFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
+            }
+        }
+    }
+
+    public void launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
+        launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
+    }
+
+    public void launchAndWaitActivity(Class fragmentClass, Options options, long waitTimeMs) {
+        launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
+    }
+
+    public void launchAndWaitActivity(String firstFragmentName, Options options, long waitTimeMs) {
+        Intent intent = new Intent();
+        intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+        if (options != null) {
+            options.collect(intent);
+        }
+        mActivity = activityTestRule.launchActivity(intent);
+        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
new file mode 100644
index 0000000..911a32e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -0,0 +1,67 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleFragmentTestActivity.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+public class SingleSupportFragmentTestActivity extends FragmentActivity {
+
+    /**
+     * Fragment that will be added to activity
+     */
+    public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+
+    public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
+    public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+
+        final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
+        if (uiOptions != 0) {
+            getWindow().getDecorView().setSystemUiVisibility(uiOptions);
+        }
+
+        setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+        if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
+            try {
+                Fragment fragment = (Fragment) Class.forName(
+                        intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+                FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+                ft.replace(R.id.main_frame, fragment);
+                ft.commit();
+            } catch (Exception ex) {
+                ex.printStackTrace();
+                finish();
+            }
+        }
+    }
+
+    public Fragment getTestFragment() {
+        return getSupportFragmentManager().findFragmentById(R.id.main_frame);
+    }
+}
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
new file mode 100644
index 0000000..a7cb793
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
@@ -0,0 +1,107 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleFrgamentTestBase.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+public class SingleSupportFragmentTestBase {
+
+    @Rule
+    public TestName mUnitTestName = new TestName();
+
+    @Rule
+    public ActivityTestRule<SingleSupportFragmentTestActivity> activityTestRule =
+            new ActivityTestRule<>(SingleSupportFragmentTestActivity.class, false, false);
+
+    protected SingleSupportFragmentTestActivity mActivity;
+
+    @After
+    public void afterTest() throws Throwable {
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                if (mActivity != null) {
+                    mActivity.finish();
+                    mActivity = null;
+                }
+            }
+        });
+    }
+
+    public void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+
+    /**
+     * Options that will be passed throught Intent to SingleSupportFragmentTestActivity
+     */
+    public static class Options {
+        int mActivityLayoutId;
+        int mUiVisibility;
+
+        public Options() {
+        }
+
+        public Options activityLayoutId(int activityLayoutId) {
+            mActivityLayoutId = activityLayoutId;
+            return this;
+        }
+
+        public Options uiVisibility(int uiVisibility) {
+            mUiVisibility = uiVisibility;
+            return this;
+        }
+
+        public void collect(Intent intent) {
+            if (mActivityLayoutId != 0) {
+                intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
+                        mActivityLayoutId);
+            }
+            if (mUiVisibility != 0) {
+                intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
+            }
+        }
+    }
+
+    public void launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
+        launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
+    }
+
+    public void launchAndWaitActivity(Class fragmentClass, Options options, long waitTimeMs) {
+        launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
+    }
+
+    public void launchAndWaitActivity(String firstFragmentName, Options options, long waitTimeMs) {
+        Intent intent = new Intent();
+        intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+        if (options != null) {
+            options.collect(intent);
+        }
+        mActivity = activityTestRule.launchActivity(intent);
+        SystemClock.sleep(waitTimeMs);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
index af56715..5c4f4fd 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
@@ -16,13 +16,10 @@
 
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
 import android.app.Fragment;
-import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -32,7 +29,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VerticalGridFragmentTest {
+public class VerticalGridFragmentTest extends SingleFragmentTestBase {
 
     public static class GridFragment extends VerticalGridFragment {
         @Override
@@ -48,31 +45,23 @@
         }
     }
 
-    public static class ImmediateRemoveFragmentActivity extends Activity {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            new Handler().postDelayed(new Runnable(){
-                public void run() {
-                    GridFragment f = new GridFragment();
-                    ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, f, null).commit();
-                    f.startEntranceTransition();
-                    ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, new Fragment(), null).commit();
-                }
-            }, 500);
-        }
-    }
-
     @Test
     public void immediateRemoveFragment() throws Throwable {
-        Intent intent = new Intent();
-        ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
-                new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
-        ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(GridFragment.class, 500);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                GridFragment f = new GridFragment();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, f, null).commit();
+                f.startEntranceTransition();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, new Fragment(), null).commit();
+            }
+        });
 
         Thread.sleep(1000);
+        mActivity.finish();
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
index 7dd402d..8b58c33 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
@@ -19,13 +19,10 @@
 
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.Fragment;
-import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -35,7 +32,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VerticalGridSupportFragmentTest {
+public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
 
     public static class GridFragment extends VerticalGridSupportFragment {
         @Override
@@ -51,31 +48,23 @@
         }
     }
 
-    public static class ImmediateRemoveFragmentActivity extends FragmentActivity {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            new Handler().postDelayed(new Runnable(){
-                public void run() {
-                    GridFragment f = new GridFragment();
-                    ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, f, null).commit();
-                    f.startEntranceTransition();
-                    ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, new Fragment(), null).commit();
-                }
-            }, 500);
-        }
-    }
-
     @Test
     public void immediateRemoveFragment() throws Throwable {
-        Intent intent = new Intent();
-        ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
-                new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
-        ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(GridFragment.class, 500);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                GridFragment f = new GridFragment();
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, f, null).commit();
+                f.startEntranceTransition();
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, new Fragment(), null).commit();
+            }
+        });
 
         Thread.sleep(1000);
+        mActivity.finish();
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
index b8bb68e..5c8c89e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
@@ -15,58 +15,103 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
 import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
 
-import org.junit.Rule;
+import junit.framework.Assert;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VideoFragmentTest {
+public class VideoFragmentTest extends SingleFragmentTestBase {
 
-    @Rule
-    public ActivityTestRule<VideoFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(VideoFragmentTestActivity.class, false, false);
-    private VideoFragmentTestActivity mActivity;
+    public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoFragment {
+        boolean mSurfaceCreated;
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+            setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+                @Override
+                public void surfaceCreated(SurfaceHolder holder) {
+                    mSurfaceCreated = true;
+                }
+
+                @Override
+                public void surfaceChanged(SurfaceHolder holder, int format, int width,
+                                           int height) {
+                }
+
+                @Override
+                public void surfaceDestroyed(SurfaceHolder holder) {
+                    mSurfaceCreated = false;
+                }
+            });
+
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+    }
 
     @Test
     public void setSurfaceViewCallbackBeforeCreate() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment1);
+        assertTrue(fragment1.mSurfaceCreated);
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                mActivity.replaceVideoFragment();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+                        .commitAllowingStateLoss();
             }
         });
+        SystemClock.sleep(500);
 
-        VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
-                R.id.video_fragment);
-        assertNotNull(fragment);
+        assertFalse(fragment1.mSurfaceCreated);
+
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment2);
+        assertTrue(fragment2.mSurfaceCreated);
+        assertNotSame(fragment1, fragment2);
     }
 
     @Test
     public void setSurfaceViewCallbackAfterCreate() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(VideoFragment.class, 1000);
+        VideoFragment fragment = (VideoFragment) mActivity.getTestFragment();
 
-        VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
-                R.id.video_fragment);
         assertNotNull(fragment);
 
+        final boolean[] surfaceCreated = new boolean[1];
         fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
             @Override
             public void surfaceCreated(SurfaceHolder holder) {
+                surfaceCreated[0] = true;
             }
 
             @Override
@@ -75,7 +120,130 @@
 
             @Override
             public void surfaceDestroyed(SurfaceHolder holder) {
+                surfaceCreated[0] = false;
             }
         });
+        assertTrue(surfaceCreated[0]);
     }
+
+    public static class Fragment_withVideoPlayer extends VideoFragment {
+        MediaPlayerGlue mGlue;
+        int mOnCreateCalled;
+        int mOnCreateViewCalled;
+        int mOnDestroyViewCalled;
+        int mOnDestroyCalled;
+        int mGlueAttachedToHost;
+        int mGlueDetachedFromHost;
+        int mGlueOnReadyForPlaybackCalled;
+
+        public Fragment_withVideoPlayer() {
+            setRetainInstance(true);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            mOnCreateCalled++;
+            super.onCreate(savedInstanceState);
+            mGlue = new MediaPlayerGlue(getActivity()) {
+                @Override
+                protected void onDetachedFromHost() {
+                    mGlueDetachedFromHost++;
+                    super.onDetachedFromHost();
+                }
+
+                @Override
+                protected void onAttachedToHost(PlaybackGlueHost host) {
+                    super.onAttachedToHost(host);
+                    mGlueAttachedToHost++;
+                }
+            };
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("Leanback");
+            mGlue.setTitle("Leanback team at work");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+                @Override
+                public void onReadyForPlayback() {
+                    mGlueOnReadyForPlaybackCalled++;
+                    mGlue.play();
+                }
+            });
+            mGlue.setHost(new VideoFragmentGlueHost(this));
+        }
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                 Bundle savedInstanceState) {
+            mOnCreateViewCalled++;
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mOnDestroyViewCalled++;
+            super.onDestroyView();
+        }
+
+        @Override
+        public void onDestroy() {
+            mOnDestroyCalled++;
+            super.onDestroy();
+        }
+    }
+
+    @Test
+    public void mediaPlayerGlueInVideoFragment() {
+        launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+        final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+                mActivity.getTestFragment();
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(1, fragment.mOnCreateViewCalled);
+        assertEquals(0, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+        View fragmentViewBeforeRecreate = fragment.getView();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.recreate();
+            }
+        });
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+            }
+        });
+        View fragmentViewAfterRecreate = fragment.getView();
+
+        Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(2, fragment.mOnCreateViewCalled);
+        assertEquals(1, fragment.mOnDestroyViewCalled);
+
+        assertEquals(1, fragment.mGlueAttachedToHost);
+        assertEquals(0, fragment.mGlueDetachedFromHost);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+        mActivity.finish();
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlueDetachedFromHost == 1;
+            }
+        });
+        assertEquals(2, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mOnDestroyCalled);
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
deleted file mode 100644
index e2a8f48..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-/**
- * Test activity containing {@link VideoFragment}.
- */
-public class VideoFragmentTestActivity extends Activity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.video_fragment_with_controls);
-    }
-
-    public void replaceVideoFragment() {
-        getFragmentManager().beginTransaction()
-                .replace(R.id.video_fragment, new VideoTestFragment())
-                .commitAllowingStateLoss();
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
new file mode 100644
index 0000000..dff3c0c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -0,0 +1,252 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class VideoSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoSupportFragment {
+        boolean mSurfaceCreated;
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+            setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+                @Override
+                public void surfaceCreated(SurfaceHolder holder) {
+                    mSurfaceCreated = true;
+                }
+
+                @Override
+                public void surfaceChanged(SurfaceHolder holder, int format, int width,
+                                           int height) {
+                }
+
+                @Override
+                public void surfaceDestroyed(SurfaceHolder holder) {
+                    mSurfaceCreated = false;
+                }
+            });
+
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+    }
+
+    @Test
+    public void setSurfaceViewCallbackBeforeCreate() {
+        launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment1);
+        assertTrue(fragment1.mSurfaceCreated);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+                        .commitAllowingStateLoss();
+            }
+        });
+        SystemClock.sleep(500);
+
+        assertFalse(fragment1.mSurfaceCreated);
+
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment2);
+        assertTrue(fragment2.mSurfaceCreated);
+        assertNotSame(fragment1, fragment2);
+    }
+
+    @Test
+    public void setSurfaceViewCallbackAfterCreate() {
+        launchAndWaitActivity(VideoSupportFragment.class, 1000);
+        VideoSupportFragment fragment = (VideoSupportFragment) mActivity.getTestFragment();
+
+        assertNotNull(fragment);
+
+        final boolean[] surfaceCreated = new boolean[1];
+        fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                surfaceCreated[0] = true;
+            }
+
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+            }
+
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {
+                surfaceCreated[0] = false;
+            }
+        });
+        assertTrue(surfaceCreated[0]);
+    }
+
+    public static class Fragment_withVideoPlayer extends VideoSupportFragment {
+        MediaPlayerGlue mGlue;
+        int mOnCreateCalled;
+        int mOnCreateViewCalled;
+        int mOnDestroyViewCalled;
+        int mOnDestroyCalled;
+        int mGlueAttachedToHost;
+        int mGlueDetachedFromHost;
+        int mGlueOnReadyForPlaybackCalled;
+
+        public Fragment_withVideoPlayer() {
+            setRetainInstance(true);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            mOnCreateCalled++;
+            super.onCreate(savedInstanceState);
+            mGlue = new MediaPlayerGlue(getActivity()) {
+                @Override
+                protected void onDetachedFromHost() {
+                    mGlueDetachedFromHost++;
+                    super.onDetachedFromHost();
+                }
+
+                @Override
+                protected void onAttachedToHost(PlaybackGlueHost host) {
+                    super.onAttachedToHost(host);
+                    mGlueAttachedToHost++;
+                }
+            };
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("Leanback");
+            mGlue.setTitle("Leanback team at work");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+                @Override
+                public void onReadyForPlayback() {
+                    mGlueOnReadyForPlaybackCalled++;
+                    mGlue.play();
+                }
+            });
+            mGlue.setHost(new VideoSupportFragmentGlueHost(this));
+        }
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                 Bundle savedInstanceState) {
+            mOnCreateViewCalled++;
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mOnDestroyViewCalled++;
+            super.onDestroyView();
+        }
+
+        @Override
+        public void onDestroy() {
+            mOnDestroyCalled++;
+            super.onDestroy();
+        }
+    }
+
+    @Test
+    public void mediaPlayerGlueInVideoSupportFragment() {
+        launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+        final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+                mActivity.getTestFragment();
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(1, fragment.mOnCreateViewCalled);
+        assertEquals(0, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+        View fragmentViewBeforeRecreate = fragment.getView();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.recreate();
+            }
+        });
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+            }
+        });
+        View fragmentViewAfterRecreate = fragment.getView();
+
+        Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(2, fragment.mOnCreateViewCalled);
+        assertEquals(1, fragment.mOnDestroyViewCalled);
+
+        assertEquals(1, fragment.mGlueAttachedToHost);
+        assertEquals(0, fragment.mGlueDetachedFromHost);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+        mActivity.finish();
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlueDetachedFromHost == 1;
+            }
+        });
+        assertEquals(2, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mOnDestroyCalled);
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
deleted file mode 100644
index a51231f3..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * {@link VideoFragment} subclass used for testing.
- */
-public class VideoTestFragment extends VideoFragment {
-    @Override
-    public View onCreateView(
-            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
-        setSurfaceHolderCallback(new SurfaceHolder.Callback() {
-            @Override
-            public void surfaceCreated(SurfaceHolder holder) {
-            }
-
-            @Override
-            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-            }
-
-            @Override
-            public void surfaceDestroyed(SurfaceHolder holder) {
-            }
-        });
-
-        return super.onCreateView(inflater, container, savedInstanceState);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java b/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
index 52995e1..132013a 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
@@ -68,7 +68,7 @@
 
         // inherit from parent
         parentDrawable.addChildDrawable(drawable);
-        parentDrawable.getChildAt(0).getBoundsRule().mBottom = BoundsRule.inheritFromParent(
+        parentDrawable.getChildAt(0).getBoundsRule().bottom = BoundsRule.inheritFromParent(
                 fraction);
         parentDrawable.updateBounds(bounds);
 
@@ -79,7 +79,7 @@
 
         // absolute value
         drawable.setBounds(bounds);
-        parentDrawable.getChildAt(0).getBoundsRule().mBottom = BoundsRule.absoluteValue(200);
+        parentDrawable.getChildAt(0).getBoundsRule().bottom = BoundsRule.absoluteValue(200);
         parentDrawable.updateBounds(bounds);
 
         adjustedBounds = drawable.getBounds();
@@ -88,7 +88,7 @@
         assertEquals(expectedBounds, adjustedBounds);
 
         // inherit with offset
-        parentDrawable.getChildAt(0).getBoundsRule().mBottom =
+        parentDrawable.getChildAt(0).getBoundsRule().bottom =
                 BoundsRule.inheritFromParentWithOffset(fraction, 100);
         parentDrawable.updateBounds(bounds);
 
@@ -99,7 +99,7 @@
 
         // inherit from parent 2
         bounds = new Rect(100, 200, WIDTH, HEIGHT);
-        parentDrawable.getChildAt(0).getBoundsRule().mBottom =
+        parentDrawable.getChildAt(0).getBoundsRule().bottom =
                 BoundsRule.inheritFromParent(fraction);
         parentDrawable.updateBounds(bounds);
 
@@ -124,9 +124,9 @@
 
         // inherit from parent
         BoundsRule boundsRule = parentDrawable.getChildAt(0).getBoundsRule();
-        boundsRule.mTop = BoundsRule.absoluteValue(-200);
-        boundsRule.mBottom = BoundsRule.inheritFromParent(fraction);
-        parentDrawable.getChildAt(0).getBoundsRule().mTop.setAbsoluteValue(-100);
+        boundsRule.top = BoundsRule.absoluteValue(-200);
+        boundsRule.bottom = BoundsRule.inheritFromParent(fraction);
+        parentDrawable.getChildAt(0).getBoundsRule().top.setAbsoluteValue(-100);
 
         parentDrawable.updateBounds(bounds);
 
@@ -137,7 +137,7 @@
         assertEquals(expectedBounds, adjustedBounds);
 
         // inherit from parent with offset
-        boundsRule.mBottom = BoundsRule.absoluteValue(HEIGHT);
+        boundsRule.bottom = BoundsRule.absoluteValue(HEIGHT);
 
         parentDrawable.updateBounds(bounds);
 
@@ -183,15 +183,15 @@
         CompositeDrawable parent = new CompositeDrawable();
         FitWidthBitmapDrawable child = new FitWidthBitmapDrawable();
         parent.addChildDrawable(child);
-        parent.getChildAt(0).getBoundsRule().mBottom =
+        parent.getChildAt(0).getBoundsRule().bottom =
                 BoundsRule.inheritFromParentWithOffset(.5f, 100);
 
         CompositeDrawable.ChildDrawable newChild = new CompositeDrawable.ChildDrawable(
                 parent.getChildAt(0),
                 parent,
                 null);
-        assertEquals(100, newChild.getBoundsRule().mBottom.getAbsoluteValue());
-        assertEquals(.5f, newChild.getBoundsRule().mBottom.getFraction(), delta);
+        assertEquals(100, newChild.getBoundsRule().bottom.getAbsoluteValue());
+        assertEquals(.5f, newChild.getBoundsRule().bottom.getFraction(), delta);
     }
 
     @Test
@@ -200,7 +200,7 @@
         CompositeDrawable parent = new CompositeDrawable();
         FitWidthBitmapDrawable child = new FitWidthBitmapDrawable();
         parent.addChildDrawable(child);
-        parent.getChildAt(0).getBoundsRule().mBottom =
+        parent.getChildAt(0).getBoundsRule().bottom =
                 BoundsRule.inheritFromParentWithOffset(.5f, 100);
 
         CompositeDrawable newDrawable = (CompositeDrawable) parent.getConstantState().newDrawable();
@@ -210,9 +210,9 @@
 
         CompositeDrawable.ChildDrawable newChild = newDrawable.getChildAt(0);
         assertNotSame(parent.getChildAt(0), newChild);
-        assertEquals(parent.getChildAt(0).getBoundsRule().mBottom.getAbsoluteValue(),
-                newChild.getBoundsRule().mBottom.getAbsoluteValue());
-        assertEquals(parent.getChildAt(0).getBoundsRule().mBottom.getFraction(),
-                newChild.getBoundsRule().mBottom.getFraction(), delta);
+        assertEquals(parent.getChildAt(0).getBoundsRule().bottom.getAbsoluteValue(),
+                newChild.getBoundsRule().bottom.getAbsoluteValue());
+        assertEquals(parent.getChildAt(0).getBoundsRule().bottom.getFraction(),
+                newChild.getBoundsRule().bottom.getFraction(), delta);
     }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
new file mode 100644
index 0000000..a2956d6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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 org.junit.Assert.assertNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MediaPlayerGlueTest {
+
+    /**
+     * Mockito spy not working on API 19 if class has package private method (b/35387610)
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mediaPlayer() {
+        // create a MediaPlayerGlue with updatePeriod = 100ms
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final MediaPlayerGlue[] result = new MediaPlayerGlue[1];
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                result[0] = new MediaPlayerGlue(context);
+            }
+        });
+        final MediaPlayerGlue glue = Mockito.spy(result[0]);
+        Mockito.when(glue.getUpdatePeriod()).thenReturn(100);
+
+        final PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        glue.setHost(host);
+        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+        final boolean[] ready = new boolean[] {false};
+        glue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            @Override
+            public void onReadyForPlayback() {
+                glue.play();
+                ready[0] = true;
+
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.setMediaSource(Uri.parse(
+                        "android.resource://android.support.v17.leanback.test/raw/track_01"));
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ready[0];
+            }
+        });
+
+        // Test enableProgressUpdating(true) and enableProgressUpdating(false);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.enableProgressUpdating(true);
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.enableProgressUpdating(false);
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, never()).updateProgress();
+
+        // Test onStart()/onStop() will pause the updateProgress.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnStart();
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnStop();
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, never()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnDestroy();
+            }
+        });
+        assertNull(glue.getHost());
+        Mockito.verify(glue, times(1)).onDetachedFromHost();
+        Mockito.verify(glue, times(1)).release();
+    }
+
+}
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
new file mode 100644
index 0000000..199ab3e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+/**
+ * Fake PlaybackGlueHost used by test.
+ */
+public class PlaybackGlueHostImpl extends PlaybackGlueHost {
+
+    HostCallback mHostCallback;
+
+    @Override
+    public void setHostCallback(HostCallback callback) {
+        mHostCallback = callback;
+    }
+
+    void notifyOnStart() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostStart();
+        }
+    }
+
+    void notifyOnStop() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostStop();
+        }
+    }
+
+    void notifyOnResume() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostResume();
+        }
+    }
+
+    void notifyOnPause() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostPause();
+        }
+    }
+
+    void notifyOnDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
new file mode 100644
index 0000000..3932ea6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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 org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PlaybackGlueTest {
+
+
+    public static class PlaybackGlueImpl extends PlaybackGlue {
+
+        public PlaybackGlueImpl(Context context) {
+            super(context);
+        }
+    }
+
+    @Test
+    public void glueAndHostInteraction() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        PlaybackGlue glue = Mockito.spy(new PlaybackGlueImpl(context));
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        glue.setHost(host);
+        Mockito.verify(glue, times(1)).onAttachedToHost(host);
+        assertSame(glue, host.mGlue);
+        assertSame(host, glue.getHost());
+
+        host.notifyOnStart();
+        Mockito.verify(glue, times(1)).onHostStart();
+
+        host.notifyOnResume();
+        Mockito.verify(glue, times(1)).onHostResume();
+
+        host.notifyOnPause();
+        Mockito.verify(glue, times(1)).onHostPause();
+
+        host.notifyOnStop();
+        Mockito.verify(glue, times(1)).onHostStop();
+
+        PlaybackGlue glue2 = Mockito.spy(new PlaybackGlueImpl(context));
+        glue2.setHost(host);
+        Mockito.verify(glue, times(1)).onDetachedFromHost();
+        Mockito.verify(glue2, times(1)).onAttachedToHost(host);
+        assertSame(glue2, host.mGlue);
+        assertSame(host, glue2.getHost());
+        assertNull(glue.getHost());
+
+        host.notifyOnDestroy();
+        assertNull(glue2.getHost());
+        assertNull(host.mGlue);
+        Mockito.verify(glue2, times(1)).onDetachedFromHost();
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerActivity.java
new file mode 100644
index 0000000..eda702f
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+
+public class DatePickerActivity extends Activity {
+
+    public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
+
+    int mLayoutId;
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mLayoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        setContentView(mLayoutId);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerTest.java
new file mode 100644
index 0000000..9a0b1c5
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/DatePickerTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.picker.DatePicker;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class DatePickerTest {
+
+    private static final String TAG = "DatePickerTest";
+    private static final long TRANSITION_LENGTH = 1000;
+
+    Context mContext;
+    View mViewAbove;
+    DatePicker mDatePickerView;
+    ViewGroup mDatePickerInnerView;
+    View mViewBelow;
+
+    @Rule
+    public ActivityTestRule<DatePickerActivity> mActivityTestRule =
+            new ActivityTestRule<>(DatePickerActivity.class, false, false);
+    private DatePickerActivity mActivity;
+
+    public void initActivity(Intent intent) throws Throwable {
+        mActivity = mActivityTestRule.launchActivity(intent);
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mDatePickerView = (DatePicker) mActivity.findViewById(R.id.date_picker);
+        mDatePickerInnerView = (ViewGroup) mDatePickerView.findViewById(R.id.picker);
+        mDatePickerView.setActivatedVisibleItemCount(3);
+        mDatePickerView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mDatePickerView.setActivated(!mDatePickerView.isActivated());
+            }
+        });
+        if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets) == R.layout.datepicker_with_other_widgets) {
+            mViewAbove = mActivity.findViewById(R.id.above_picker);
+            mViewBelow = mActivity.findViewById(R.id.below_picker);
+        } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets) == R.layout.datepicker_alone) {
+            // A layout with only a DatePicker widget that is initially activated.
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mDatePickerView.setActivated(true);
+                }
+            });
+            Thread.sleep(500);
+        }
+    }
+
+    @Test
+    public void testFocusTravel() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+
+        assertThat("TextView above should have focus initially", mViewAbove.hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("DatePicker should have focus now", mDatePickerView.isFocused(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The first column of DatePicker should hold focus",
+                mDatePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // skipping the separator in the child indices
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of DatePicker should hold focus",
+                mDatePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of DatePicker should hold focus",
+                mDatePickerInnerView.getChildAt(4).hasFocus(), is(true));
+    }
+
+    @Test
+    public void testFocusRetainedForASelectedColumn()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+        mDatePickerView.setFocusable(true);
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("DatePicker should have focus when it's focusable",
+                mDatePickerView.isFocused(), is(true));
+
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("After the first activation, the first column of DatePicker should hold focus",
+                mDatePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of DatePicker should hold focus",
+                mDatePickerInnerView.getChildAt(4).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("After the first deactivation, the DatePicker itself should hold focus",
+                mDatePickerView.isFocused(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("After the second activation, the last selected column (3rd) should hold focus",
+                mDatePickerInnerView.getChildAt(4).hasFocus(), is(true));
+    }
+
+    @Test
+    public void testFocusSkippedWhenDatePickerUnFocusable()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+
+        mDatePickerView.setFocusable(false);
+        assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("DatePicker should be skipped and TextView below should have focus.",
+                mViewBelow.hasFocus(), is(true));
+    }
+
+    @Test
+    public void testTemporaryFocusLossWhenDeactivated()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+
+        final int[] currentFocusChangeCountForViewAbove = {0};
+        mDatePickerView.setFocusable(true);
+        Log.d(TAG, "view above: " + mViewAbove);
+        mViewAbove.setOnFocusChangeListener(new View.OnFocusChangeListener(){
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                currentFocusChangeCountForViewAbove[0]++;
+            }
+        });
+        assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true));
+
+        // Traverse to the third column of date picker
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Traverse to the third column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click to deactivate. Before that we remember the focus change count for the view above.
+        // This view should NOT receive temporary focus when DatePicker is deactivated, and
+        // DatePicker itself should capture the focus.
+        int[] lastFocusChangeCountForViewAbove = {currentFocusChangeCountForViewAbove[0]};
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("DatePicker should have focus now since it's focusable",
+                mDatePickerView.isFocused(), is(true));
+        assertThat("Focus change count of view above should not be changed after last click.",
+                currentFocusChangeCountForViewAbove[0], is(lastFocusChangeCountForViewAbove[0]));
+    }
+
+    @Test
+    public void testTemporaryFocusLossWhenActivated() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+        final int[] currentFocusChangeCountForColumns = {0, 0, 0};
+        mDatePickerView.setFocusable(true);
+        mDatePickerInnerView.getChildAt(0).setOnFocusChangeListener(
+                new View.OnFocusChangeListener() {
+                    @Override
+                    public void onFocusChange(View v, boolean hasFocus) {
+                        currentFocusChangeCountForColumns[0]++;
+                    }
+                });
+
+        mDatePickerInnerView.getChildAt(2).setOnFocusChangeListener(
+                new View.OnFocusChangeListener() {
+                    @Override
+                    public void onFocusChange(View v, boolean hasFocus) {
+                        currentFocusChangeCountForColumns[1]++;
+                    }
+                });
+
+        mDatePickerInnerView.getChildAt(4).setOnFocusChangeListener(
+                new View.OnFocusChangeListener() {
+                    @Override
+                    public void onFocusChange(View v, boolean hasFocus) {
+                        currentFocusChangeCountForColumns[2]++;
+                    }
+                });
+
+        // Traverse to the third column of date picker
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Traverse to the third column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click to deactivate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click again. The focus should NOT be temporarily moved to the other columns and the third
+        // column should receive focus.
+        // Before that we will remember the last focus change count to compare it against after the
+        // click.
+        int[] lastFocusChangeCountForColumns = {currentFocusChangeCountForColumns[0],
+                currentFocusChangeCountForColumns[1], currentFocusChangeCountForColumns[2]};
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("Focus change count of column 0 should not be changed after last click.",
+                currentFocusChangeCountForColumns[0], is(lastFocusChangeCountForColumns[0]));
+        assertThat("Focus change count of column 1 should not be changed after last click.",
+                currentFocusChangeCountForColumns[1], is(lastFocusChangeCountForColumns[1]));
+        assertThat("Focus change count of column 2 should not be changed after last click.",
+                currentFocusChangeCountForColumns[2], is(lastFocusChangeCountForColumns[2]));
+    }
+
+    @Test
+    public void testInitiallyActiveDatePicker()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_alone);
+        initActivity(intent);
+
+        assertThat("The first column of DatePicker should initially hold focus",
+                mDatePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on first column
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The first column of DatePicker should still hold focus after scrolling down",
+                mDatePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on second column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of DatePicker should hold focus after scrolling right",
+                mDatePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of DatePicker should still hold focus after scrolling down",
+                mDatePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        // focus on third column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of DatePicker should hold focus after scrolling right",
+                mDatePickerInnerView.getChildAt(4).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of DatePicker should still hold focus after scrolling down",
+                mDatePickerInnerView.getChildAt(4).hasFocus(), is(true));
+    }
+
+    @Test
+    public void testInvisibleColumnsAlpha() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.datepicker_with_other_widgets);
+        initActivity(intent);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDatePickerView.updateDate(2017, 2, 21, false);
+            }
+        });
+
+        Thread.sleep(TRANSITION_LENGTH);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDatePickerView.updateDate(2017, 2, 20, false);
+            }
+        });
+        Thread.sleep(TRANSITION_LENGTH);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        int activeColumn = 0;
+        // For the inactive columns: the alpha for all the rows except the selected row should be
+        // zero: Picker#mInvisibleColumnAlpha (they should all be invisible).
+        for (int i = 0; i < 3; i++) {
+            ViewGroup gridView = (ViewGroup) mDatePickerInnerView.getChildAt(2 * i);
+            int childCount = gridView.getChildCount();
+            int alpha1RowsCount = 0;
+            int alphaNonZeroRowsCount = 0;
+            for (int j = 0; j < childCount; j++) {
+                View pickerItem = gridView.getChildAt(j);
+                if (pickerItem.getAlpha() > 0) {
+                    alphaNonZeroRowsCount++;
+                }
+                if (pickerItem.getAlpha() == 1) {
+                    alpha1RowsCount++;
+                }
+            }
+            if (i == activeColumn) {
+                assertThat("The active column "  + i + " should have only one row with an alpha of "
+                                + "1", alpha1RowsCount, is(1));
+                assertTrue("The active column "  + i + " should have more than one view with alpha "
+                        + "greater than 1", alphaNonZeroRowsCount > 1);
+            } else {
+                assertThat("The inactive column " + i + " should have only one row with an alpha of"
+                        + " 1", alpha1RowsCount, is(1));
+                assertThat("The inactive column " + i + " should have only one row with a non-zero"
+                        + " alpha", alphaNonZeroRowsCount, is(1));
+            }
+        }
+    }
+
+    private void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
new file mode 100644
index 0000000..4b075ea
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
@@ -0,0 +1,327 @@
+package android.support.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Parcelable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridWidgetPrefetchTest {
+
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    private void layout(View view, int width, int height) {
+        view.measure(
+                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
+        view.layout(0, 0, width, height);
+    }
+
+    public void validatePrefetch(BaseGridView gridView, int scrollX, int scrollY,
+            Integer[]... positionData) {
+        // duplicates logic in support.v7.widget.CacheUtils#verifyPositionsPrefetched
+        RecyclerView.State state = mock(RecyclerView.State.class);
+        when(state.getItemCount()).thenReturn(gridView.getAdapter().getItemCount());
+        RecyclerView.LayoutManager.LayoutPrefetchRegistry registry
+                = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class);
+
+        gridView.getLayoutManager().collectAdjacentPrefetchPositions(scrollX, scrollY,
+                state, registry);
+
+        verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt());
+        for (Integer[] aPositionData : positionData) {
+            verify(registry).addPosition(aPositionData[0], aPositionData[1]);
+        }
+    }
+
+    private RecyclerView.Adapter createBoxAdapter() {
+        return new RecyclerView.Adapter() {
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                View view = new View(getContext());
+                view.setMinimumWidth(100);
+                view.setMinimumHeight(100);
+                return new RecyclerView.ViewHolder(view) {};
+            }
+
+            @Override
+            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+                // noop
+            }
+
+            @Override
+            public int getItemCount() {
+                return 100;
+            }
+        };
+    }
+
+    @Test
+    public void prefetch() {
+        HorizontalGridView gridView = new HorizontalGridView(getContext());
+        gridView.setNumRows(1);
+        gridView.setRowHeight(100);
+        gridView.setAdapter(createBoxAdapter());
+
+        layout(gridView, 150, 100);
+
+        // validate 2 children in viewport
+        assertEquals(2, gridView.getChildCount());
+        assertEquals(0, gridView.getLayoutManager().findViewByPosition(0).getLeft());
+        assertEquals(100, gridView.getLayoutManager().findViewByPosition(1).getLeft());
+
+        validatePrefetch(gridView, -50, 0); // no view to left
+        validatePrefetch(gridView, 50, 0, new Integer[] {2, 50}); // next view 50 pixels to right
+
+        // scroll to position 5, and layout
+        gridView.scrollToPosition(5);
+        layout(gridView, 150, 100);
+
+        /* Visual representation, each number column represents 25 pixels:
+         *              |           |
+         * ... 3 3 4 4 4|4 5 5 5 5 6|6 6 6 7 7 ...
+         *              |           |
+         */
+
+        // validate the 3 children in the viewport, and their positions
+        assertEquals(3, gridView.getChildCount());
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(4));
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(5));
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(6));
+        assertEquals(-75, gridView.getLayoutManager().findViewByPosition(4).getLeft());
+        assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft());
+        assertEquals(125, gridView.getLayoutManager().findViewByPosition(6).getLeft());
+
+        // next views are 75 pixels to right and left:
+        validatePrefetch(gridView, -50, 0, new Integer[] {3, 75});
+        validatePrefetch(gridView, 50, 0, new Integer[] {7, 75});
+
+        // no views returned for vertical prefetch:
+        validatePrefetch(gridView, 0, 10);
+        validatePrefetch(gridView, 0, -10);
+
+        // test minor offset
+        gridView.scrollBy(5, 0);
+        validatePrefetch(gridView, -50, 0, new Integer[] {3, 80});
+        validatePrefetch(gridView, 50, 0, new Integer[] {7, 70});
+    }
+
+    @Test
+    public void prefetchRtl() {
+        HorizontalGridView gridView = new HorizontalGridView(getContext());
+        gridView.setNumRows(1);
+        gridView.setRowHeight(100);
+        gridView.setAdapter(createBoxAdapter());
+        gridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
+
+        layout(gridView, 150, 100);
+
+        // validate 2 children in viewport
+        assertEquals(2, gridView.getChildCount());
+        assertEquals(50, gridView.getLayoutManager().findViewByPosition(0).getLeft());
+        assertEquals(-50, gridView.getLayoutManager().findViewByPosition(1).getLeft());
+
+        validatePrefetch(gridView, 50, 0); // no view to right
+        validatePrefetch(gridView, -10, 0, new Integer[] {2, 50}); // next view 50 pixels to right
+
+
+        // scroll to position 5, and layout
+        gridView.scrollToPosition(5);
+        layout(gridView, 150, 100);
+
+
+        /* Visual representation, each number column represents 25 pixels:
+         *              |           |
+         * ... 7 7 6 6 6|6 5 5 5 5 4|4 4 4 3 3 ...
+         *              |           |
+         */
+        // validate 3 children in the viewport
+        assertEquals(3, gridView.getChildCount());
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(6));
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(5));
+        assertNotNull(gridView.getLayoutManager().findViewByPosition(4));
+        assertEquals(-75, gridView.getLayoutManager().findViewByPosition(6).getLeft());
+        assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft());
+        assertEquals(125, gridView.getLayoutManager().findViewByPosition(4).getLeft());
+
+        // next views are 75 pixels to right and left:
+        validatePrefetch(gridView, 50, 0, new Integer[] {3, 75});
+        validatePrefetch(gridView, -50, 0, new Integer[] {7, 75});
+
+        // no views returned for vertical prefetch:
+        validatePrefetch(gridView, 0, 10);
+        validatePrefetch(gridView, 0, -10);
+
+        // test minor offset
+        gridView.scrollBy(-5, 0);
+        validatePrefetch(gridView, 50, 0, new Integer[] {3, 80});
+        validatePrefetch(gridView, -50, 0, new Integer[] {7, 70});
+    }
+
+
+    class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
+        OuterAdapter() {
+            for (int i = 0; i < getItemCount(); i++) {
+                mAdapters.add(createBoxAdapter());
+                mSavedStates.add(null);
+            }
+        }
+
+        class ViewHolder extends RecyclerView.ViewHolder {
+            private final RecyclerView mRecyclerView;
+            ViewHolder(RecyclerView itemView) {
+                super(itemView);
+                mRecyclerView = itemView;
+            }
+        }
+
+        ArrayList<RecyclerView.Adapter> mAdapters = new ArrayList<>();
+        ArrayList<Parcelable> mSavedStates = new ArrayList<>();
+        RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
+
+        @Override
+        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            HorizontalGridView gridView = new HorizontalGridView(getContext());
+            gridView.setNumRows(1);
+            gridView.setRowHeight(100);
+            gridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+            gridView.setLayoutParams(new GridLayoutManager.LayoutParams(350, 100));
+            gridView.setRecycledViewPool(mSharedPool);
+            return new ViewHolder(gridView);
+        }
+
+        @Override
+        public void onBindViewHolder(ViewHolder holder, int position) {
+            holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);
+
+            Parcelable savedState = mSavedStates.get(position);
+            if (savedState != null) {
+                holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
+                mSavedStates.set(position, null);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return 100;
+        }
+    };
+
+    public void validateInitialPrefetch(BaseGridView gridView,
+            int... positionData) {
+        RecyclerView.LayoutManager.LayoutPrefetchRegistry registry
+                = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class);
+        gridView.getLayoutManager().collectInitialPrefetchPositions(
+                gridView.getAdapter().getItemCount(), registry);
+
+        verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt());
+        for (int position : positionData) {
+            verify(registry).addPosition(position, 0);
+        }
+    }
+
+    @Test
+    public void prefetchInitialFocusTest() {
+        VerticalGridView view = new VerticalGridView(getContext());
+        view.setNumColumns(1);
+        view.setColumnWidth(350);
+        view.setAdapter(createBoxAdapter());
+
+        // check default
+        assertEquals(4, view.getInitialItemPrefetchCount());
+
+        // check setter behavior
+        view.setInitialPrefetchItemCount(0);
+        assertEquals(0, view.getInitialItemPrefetchCount());
+
+        // check positions fetched, relative to focus
+        view.scrollToPosition(2);
+        view.setInitialPrefetchItemCount(5);
+        validateInitialPrefetch(view, 0, 1, 2, 3, 4);
+
+        view.setInitialPrefetchItemCount(3);
+        validateInitialPrefetch(view, 1, 2, 3);
+
+        view.scrollToPosition(0);
+        view.setInitialPrefetchItemCount(4);
+        validateInitialPrefetch(view, 0, 1, 2, 3);
+
+        view.scrollToPosition(98);
+        view.setInitialPrefetchItemCount(5);
+        validateInitialPrefetch(view, 95, 96, 97, 98, 99);
+
+        view.setInitialPrefetchItemCount(7);
+        validateInitialPrefetch(view, 93, 94, 95, 96, 97, 98, 99);
+
+        // implementation detail - rounds up
+        view.scrollToPosition(50);
+        view.setInitialPrefetchItemCount(4);
+        validateInitialPrefetch(view, 49, 50, 51, 52);
+    }
+
+    @Test
+    public void prefetchNested() {
+        VerticalGridView gridView = new VerticalGridView(getContext());
+        gridView.setNumColumns(1);
+        gridView.setColumnWidth(350);
+        OuterAdapter outerAdapter = new OuterAdapter();
+        gridView.setAdapter(outerAdapter);
+        gridView.setItemViewCacheSize(1); // enough to cache child 0 while offscreen
+
+        layout(gridView, 350, 150);
+
+        // validate 2 top level children in viewport
+        assertEquals(2, gridView.getChildCount());
+        for (int y = 0; y < 2; y++) {
+            View child = gridView.getLayoutManager().findViewByPosition(y);
+            assertEquals(y * 100, child.getTop());
+            // each has 4 children
+
+            HorizontalGridView inner = (HorizontalGridView) child;
+            for (int x = 0; x < 4; x++) {
+                assertEquals(x * 100, inner.getLayoutManager().findViewByPosition(x).getLeft());
+            }
+        }
+
+        // center child 0 at position 10
+        HorizontalGridView offsetChild =
+                (HorizontalGridView) gridView.getLayoutManager().findViewByPosition(0);
+        offsetChild.scrollToPosition(10);
+
+        // scroll to position 2, and layout
+        gridView.scrollToPosition(2);
+        layout(gridView, 350, 150);
+
+        // now, offset by 175, centered around row 2. Validate 3 top level children in viewport
+        assertEquals(3, gridView.getChildCount());
+        for (int y = 1; y < 4; y++) {
+            assertEquals(y * 100 - 175, gridView.getLayoutManager().findViewByPosition(y).getTop());
+        }
+
+        validatePrefetch(gridView, 0, -5, new Integer[] {0, 75});
+        validatePrefetch(gridView, 0, 5, new Integer[] {4, 75});
+
+        // assume offsetChild still bound, in cache, just not attached...
+        validateInitialPrefetch(offsetChild, 9, 10, 11, 12);
+    }
+}
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 8a38894..7dab382 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
@@ -21,7 +21,6 @@
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
@@ -40,6 +39,7 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
@@ -2988,4 +2988,193 @@
         assertTrue(selectedPosition2 < selectedPosition1);
     }
 
+    @Test
+    public void testAnimateOutResetByScrollTo() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.scrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+    }
+
+    @Test
+    public void testAnimateOutResetByFocusChange() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+                mActivity.findViewById(R.id.button).requestFocus();
+            }
+        });
+        assertTrue(mActivity.findViewById(R.id.button).hasFocus());
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.requestFocus();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+    }
+
+    @Test
+    public void testHorizontalAnimateOutResetByScrollTo() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+                mGridView.getChildAt(0).getLeft());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getLeft() > mGridView.getPaddingLeft();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.scrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+                mGridView.getChildAt(0).getLeft());
+    }
+
+    @Test
+    public void testHorizontalAnimateOutRtl() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear_rtl);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding right",
+                mGridView.getWidth() - mGridView.getPaddingRight(),
+                mGridView.getChildAt(0).getRight());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getRight()
+                        < mGridView.getWidth() - mGridView.getPaddingRight();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.smoothScrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding right",
+                mGridView.getWidth() - mGridView.getPaddingRight(),
+                mGridView.getChildAt(0).getRight());
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
index 9347776..0c98e7d 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
@@ -36,21 +36,18 @@
 @SmallTest
 public class ParallaxFloatEffectTest {
 
-    ParallaxSource.FloatSource source;
-    float screenMax;
-    ParallaxEffect.FloatEffect effect;
-    @Mock ParallaxTarget target;
+    Parallax.FloatParallax mSource;
+    int mScreenMax;
+    ParallaxEffect.FloatEffect mEffect;
+    @Mock ParallaxTarget mTarget;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        source = new ParallaxSource.FloatSource<ParallaxSource.FloatProperty>() {
+        mSource = new Parallax.FloatParallax<Parallax.FloatProperty>() {
 
-            public void setListener(ParallaxSource.Listener listener) {
-            }
-
-            public float getMaxParentVisibleSize() {
-                return screenMax;
+            public float getMaxValue() {
+                return mScreenMax;
             }
 
             @Override
@@ -58,182 +55,182 @@
                 return new FloatProperty(name, index);
             }
         };
-        effect = new ParallaxEffect.FloatEffect();
+        mEffect = new ParallaxEffect.FloatEffect();
     }
 
     @Test
     public void testOneVariable() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(0));
-        effect.target(target);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(0));
+        mEffect.target(mTarget);
 
         // start
-        var1.setFloatValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // 25% complete
-        var1.setFloatValue(source, 405);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.25f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 405);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.25f);
+        Mockito.reset(mTarget);
 
         // middle
-        var1.setFloatValue(source, 270);
-        effect.performMapping(source);
-        verify(target, times(1)).update(.5f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 270);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(.5f);
+        Mockito.reset(mTarget);
 
         // 75% complete
-        var1.setFloatValue(source, 135);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.75f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 135);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.75f);
+        Mockito.reset(mTarget);
 
         // end
-        var1.setFloatValue(source, 0);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 0);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // after end
-        var1.setFloatValue(source, -1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, -1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // before start
-        var1.setFloatValue(source, 1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
     }
 
     @Test(expected=IllegalStateException.class)
     public void testVerifyKeyValueOfSameVariableInDesendantOrder() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(550));
-        effect.target(target);
-        var1.setFloatValue(source, 0);
-        effect.performMapping(source);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(550));
+        mEffect.target(mTarget);
+        var1.setFloatValue(mSource, 0);
+        mEffect.performMapping(mSource);
     }
 
     @Test
     public void testTwoVariable() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-        ParallaxSource.FloatProperty var2 = source.addProperty("var2");
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+        Parallax.FloatProperty var2 = mSource.addProperty("var2");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var2.atAbsolute(540));
-        effect.target(target);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var2.atAbsolute(540));
+        mEffect.target(mTarget);
 
         // start
-        var1.setFloatValue(source, 540);
-        var2.setFloatValue(source, 840);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 540);
+        var2.setFloatValue(mSource, 840);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // middle
-        var1.setFloatValue(source, 390);
-        var2.setFloatValue(source, 690);
-        effect.performMapping(source);
-        verify(target, times(1)).update(.5f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 390);
+        var2.setFloatValue(mSource, 690);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(.5f);
+        Mockito.reset(mTarget);
 
         // end
-        var1.setFloatValue(source, 240);
-        var2.setFloatValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 240);
+        var2.setFloatValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // after end
-        var1.setFloatValue(source, 200);
-        var2.setFloatValue(source, 500);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 200);
+        var2.setFloatValue(mSource, 500);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // before start
-        var1.setFloatValue(source, 1000);
-        var2.setFloatValue(source, 1300);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 1000);
+        var2.setFloatValue(mSource, 1300);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, -1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, -1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setFloatValue(source, 1000);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 1000);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before and less
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, 500);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, 500);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before and hit second
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before with estimation
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, 1080);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.5f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, 1080);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.5f);
+        Mockito.reset(mTarget);
 
         // unknown_after with estimation
-        var1.setFloatValue(source, 0);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.5f);
-        Mockito.reset(target);
+        var1.setFloatValue(mSource, 0);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.5f);
+        Mockito.reset(mTarget);
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatSourceTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatSourceTest.java
deleted file mode 100644
index 087e1b7..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatSourceTest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from ParallaxIntSourceTest.java.  DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.widget;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ParallaxFloatSourceTest {
-
-    ParallaxSource.FloatSource source;
-    float screenMax;
-
-    static void assertFloatEquals(float expected, float actual) {
-        org.junit.Assert.assertEquals((double)expected, (double)actual, 0.0001d);
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        source = new ParallaxSource.FloatSource<ParallaxSource.FloatProperty>() {
-
-            public void setListener(ParallaxSource.Listener listener) {
-            }
-
-            public float getMaxParentVisibleSize() {
-                return screenMax;
-            }
-
-            @Override
-            public FloatProperty createProperty(String name, int index) {
-                return new FloatProperty(name, index);
-            }
-        };
-    }
-
-    @Test
-    public void testVariable() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-        var1.setFloatValue(source, 54);
-        assertFloatEquals((float)54, var1.getFloatValue(source));
-        assertEquals(var1.getName(), "var1");
-        var1.set(source, (float)2000);
-        assertFloatEquals((float)2000, var1.get(source).floatValue());
-    }
-
-    @Test
-    public void testFixedKeyValue() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.FloatPropertyKeyValue keyValue = var1.atAbsolute(1000);
-        assertSame(keyValue.getProperty(), var1);
-        assertFloatEquals((float)1000, keyValue.getKeyValue(source));
-    }
-
-    @Test
-    public void testFractionOfKeyValue() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.FloatPropertyKeyValue keyValue = var1.at(0, 0.5f);
-        assertSame(keyValue.getProperty(), var1);
-        assertFloatEquals((float)540, keyValue.getKeyValue(source));
-    }
-
-    @Test
-    public void testFixedKeyValueWithFraction() {
-        screenMax = 1080;
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.FloatPropertyKeyValue keyValue = var1.at(-100, 0.5f);
-        assertSame(keyValue.getProperty(), var1);
-        assertFloatEquals((float)440, keyValue.getKeyValue(source));
-
-        ParallaxSource.FloatPropertyKeyValue keyValue2 = var1.at(100, 0.5f);
-        assertSame(keyValue2.getProperty(), var1);
-        assertFloatEquals((float)640, keyValue2.getKeyValue(source));
-    }
-
-    @Test(expected=IllegalStateException.class)
-    public void testVerifyFloatPropertys_wrongOrder() {
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-        ParallaxSource.FloatProperty var2 = source.addProperty("var2");;
-
-        var1.setFloatValue(source, (float)500);
-        var2.setFloatValue(source, (float)499);
-
-        source.verifyProperties();
-    }
-
-    @Test(expected=IllegalStateException.class)
-    public void testVerifyFloatPropertysWrong_combination() {
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-        ParallaxSource.FloatProperty var2 = source.addProperty("var2");
-
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-
-        source.verifyProperties();
-    }
-
-    @Test
-    public void testVerifyFloatPropertys_success() {
-        ParallaxSource.FloatProperty var1 = source.addProperty("var1");
-        ParallaxSource.FloatProperty var2 = source.addProperty("var2");
-
-        var1.setFloatValue(source, (float)499);
-        var2.setFloatValue(source, (float)500);
-
-        source.verifyProperties();
-
-        var1.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_BEFORE);
-        var2.setFloatValue(source, (float)500);
-
-        source.verifyProperties();
-
-        var1.setFloatValue(source, (float)499);
-        var2.setFloatValue(source, ParallaxSource.FloatProperty.UNKNOWN_AFTER);
-
-        source.verifyProperties();
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
new file mode 100644
index 0000000..a2b00b9
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
@@ -0,0 +1,144 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from ParallaxIntTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ParallaxFloatTest {
+
+    Parallax.FloatParallax mSource;
+    int mScreenMax;
+
+    static void assertFloatEquals(float expected, float actual) {
+        org.junit.Assert.assertEquals((double) expected, (double) actual, 0.0001d);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mSource = new Parallax.FloatParallax<Parallax.FloatProperty>() {
+
+            public float getMaxValue() {
+                return mScreenMax;
+            }
+
+            @Override
+            public FloatProperty createProperty(String name, int index) {
+                return new FloatProperty(name, index);
+            }
+        };
+    }
+
+    @Test
+    public void testVariable() {
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+        var1.setFloatValue(mSource, 54);
+        assertFloatEquals((float) 54, var1.getFloatValue(mSource));
+        assertEquals(var1.getName(), "var1");
+        var1.set(mSource, (float) 2000);
+        assertFloatEquals((float) 2000, var1.get(mSource).floatValue());
+    }
+
+    @Test
+    public void testFixedKeyValue() {
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+
+        Parallax.FloatPropertyMarkerValue keyValue = var1.atAbsolute(1000);
+        assertSame(keyValue.getProperty(), var1);
+        assertFloatEquals((float) 1000, keyValue.getMarkerValue(mSource));
+    }
+
+    @Test
+    public void testFractionOfKeyValue() {
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+
+        Parallax.FloatPropertyMarkerValue keyValue = var1.at(0, 0.5f);
+        assertSame(keyValue.getProperty(), var1);
+        assertFloatEquals((float) 540, keyValue.getMarkerValue(mSource));
+    }
+
+    @Test
+    public void testFixedKeyValueWithFraction() {
+        mScreenMax = 1080;
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+
+        Parallax.FloatPropertyMarkerValue keyValue = var1.at(-100, 0.5f);
+        assertSame(keyValue.getProperty(), var1);
+        assertFloatEquals((float) 440, keyValue.getMarkerValue(mSource));
+
+        Parallax.FloatPropertyMarkerValue keyValue2 = var1.at(100, 0.5f);
+        assertSame(keyValue2.getProperty(), var1);
+        assertFloatEquals((float) 640, keyValue2.getMarkerValue(mSource));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testVerifyFloatPropertys_wrongOrder() {
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+        Parallax.FloatProperty var2 = mSource.addProperty("var2");
+
+        var1.setFloatValue(mSource, (float) 500);
+        var2.setFloatValue(mSource, (float) 499);
+
+        mSource.verifyProperties();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testVerifyFloatPropertysWrong_combination() {
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+        Parallax.FloatProperty var2 = mSource.addProperty("var2");
+
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+
+        mSource.verifyProperties();
+    }
+
+    @Test
+    public void testVerifyFloatPropertys_success() {
+        Parallax.FloatProperty var1 = mSource.addProperty("var1");
+        Parallax.FloatProperty var2 = mSource.addProperty("var2");
+
+        var1.setFloatValue(mSource, (float) 499);
+        var2.setFloatValue(mSource, (float) 500);
+
+        mSource.verifyProperties();
+
+        var1.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_BEFORE);
+        var2.setFloatValue(mSource, (float) 500);
+
+        mSource.verifyProperties();
+
+        var1.setFloatValue(mSource, (float) 499);
+        var2.setFloatValue(mSource, Parallax.FloatProperty.UNKNOWN_AFTER);
+
+        mSource.verifyProperties();
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
index 59ba1bc..02b3f69 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
@@ -33,21 +33,18 @@
 @SmallTest
 public class ParallaxIntEffectTest {
 
-    ParallaxSource.IntSource source;
-    int screenMax;
-    ParallaxEffect.IntEffect effect;
-    @Mock ParallaxTarget target;
+    Parallax.IntParallax mSource;
+    int mScreenMax;
+    ParallaxEffect.IntEffect mEffect;
+    @Mock ParallaxTarget mTarget;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        source = new ParallaxSource.IntSource<ParallaxSource.IntProperty>() {
+        mSource = new Parallax.IntParallax<Parallax.IntProperty>() {
 
-            public void setListener(ParallaxSource.Listener listener) {
-            }
-
-            public int getMaxParentVisibleSize() {
-                return screenMax;
+            public int getMaxValue() {
+                return mScreenMax;
             }
 
             @Override
@@ -55,182 +52,182 @@
                 return new IntProperty(name, index);
             }
         };
-        effect = new ParallaxEffect.IntEffect();
+        mEffect = new ParallaxEffect.IntEffect();
     }
 
     @Test
     public void testOneVariable() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(0));
-        effect.target(target);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(0));
+        mEffect.target(mTarget);
 
         // start
-        var1.setIntValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // 25% complete
-        var1.setIntValue(source, 405);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.25f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 405);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.25f);
+        Mockito.reset(mTarget);
 
         // middle
-        var1.setIntValue(source, 270);
-        effect.performMapping(source);
-        verify(target, times(1)).update(.5f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 270);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(.5f);
+        Mockito.reset(mTarget);
 
         // 75% complete
-        var1.setIntValue(source, 135);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.75f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 135);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.75f);
+        Mockito.reset(mTarget);
 
         // end
-        var1.setIntValue(source, 0);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 0);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // after end
-        var1.setIntValue(source, -1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, -1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // before start
-        var1.setIntValue(source, 1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
     }
 
     @Test(expected=IllegalStateException.class)
     public void testVerifyKeyValueOfSameVariableInDesendantOrder() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(550));
-        effect.target(target);
-        var1.setIntValue(source, 0);
-        effect.performMapping(source);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var1.atAbsolute(550));
+        mEffect.target(mTarget);
+        var1.setIntValue(mSource, 0);
+        mEffect.performMapping(mSource);
     }
 
     @Test
     public void testTwoVariable() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-        ParallaxSource.IntProperty var2 = source.addProperty("var2");
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+        Parallax.IntProperty var2 = mSource.addProperty("var2");
 
-        effect.setPropertyRanges(var1.atAbsolute(540), var2.atAbsolute(540));
-        effect.target(target);
+        mEffect.setPropertyRanges(var1.atAbsolute(540), var2.atAbsolute(540));
+        mEffect.target(mTarget);
 
         // start
-        var1.setIntValue(source, 540);
-        var2.setIntValue(source, 840);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 540);
+        var2.setIntValue(mSource, 840);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // middle
-        var1.setIntValue(source, 390);
-        var2.setIntValue(source, 690);
-        effect.performMapping(source);
-        verify(target, times(1)).update(.5f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 390);
+        var2.setIntValue(mSource, 690);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(.5f);
+        Mockito.reset(mTarget);
 
         // end
-        var1.setIntValue(source, 240);
-        var2.setIntValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 240);
+        var2.setIntValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // after end
-        var1.setIntValue(source, 200);
-        var2.setIntValue(source, 500);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 200);
+        var2.setIntValue(mSource, 500);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // before start
-        var1.setIntValue(source, 1000);
-        var2.setIntValue(source, 1300);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 1000);
+        var2.setIntValue(mSource, 1300);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, -1000);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, -1000);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_after
-        var1.setIntValue(source, 1000);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 1000);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0f);
+        Mockito.reset(mTarget);
 
         // unknown_before and less
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, 500);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, 500);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before and hit second
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, 540);
-        effect.performMapping(source);
-        verify(target, times(1)).update(1f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, 540);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(1f);
+        Mockito.reset(mTarget);
 
         // unknown_before with estimation
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, 1080);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.5f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, 1080);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.5f);
+        Mockito.reset(mTarget);
 
         // unknown_after with estimation
-        var1.setIntValue(source, 0);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-        effect.performMapping(source);
-        verify(target, times(1)).update(0.5f);
-        Mockito.reset(target);
+        var1.setIntValue(mSource, 0);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+        mEffect.performMapping(mSource);
+        verify(mTarget, times(1)).update(0.5f);
+        Mockito.reset(mTarget);
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntSourceTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntSourceTest.java
deleted file mode 100644
index f7097ec..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntSourceTest.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.widget;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ParallaxIntSourceTest {
-
-    ParallaxSource.IntSource source;
-    int screenMax;
-
-    static void assertFloatEquals(float expected, float actual) {
-        org.junit.Assert.assertEquals((double)expected, (double)actual, 0.0001d);
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        source = new ParallaxSource.IntSource<ParallaxSource.IntProperty>() {
-
-            public void setListener(ParallaxSource.Listener listener) {
-            }
-
-            public int getMaxParentVisibleSize() {
-                return screenMax;
-            }
-
-            @Override
-            public IntProperty createProperty(String name, int index) {
-                return new IntProperty(name, index);
-            }
-        };
-    }
-
-    @Test
-    public void testVariable() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-        var1.setIntValue(source, 54);
-        assertEquals((int)54, var1.getIntValue(source));
-        assertEquals(var1.getName(), "var1");
-        var1.set(source, (int)2000);
-        assertEquals((int)2000, var1.get(source).intValue());
-    }
-
-    @Test
-    public void testFixedKeyValue() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.IntPropertyKeyValue keyValue = var1.atAbsolute(1000);
-        assertSame(keyValue.getProperty(), var1);
-        assertEquals((int)1000, keyValue.getKeyValue(source));
-    }
-
-    @Test
-    public void testFractionOfKeyValue() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.IntPropertyKeyValue keyValue = var1.at(0, 0.5f);
-        assertSame(keyValue.getProperty(), var1);
-        assertEquals((int)540, keyValue.getKeyValue(source));
-    }
-
-    @Test
-    public void testFixedKeyValueWithFraction() {
-        screenMax = 1080;
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-
-        ParallaxSource.IntPropertyKeyValue keyValue = var1.at(-100, 0.5f);
-        assertSame(keyValue.getProperty(), var1);
-        assertEquals((int)440, keyValue.getKeyValue(source));
-
-        ParallaxSource.IntPropertyKeyValue keyValue2 = var1.at(100, 0.5f);
-        assertSame(keyValue2.getProperty(), var1);
-        assertEquals((int)640, keyValue2.getKeyValue(source));
-    }
-
-    @Test(expected=IllegalStateException.class)
-    public void testVerifyIntPropertys_wrongOrder() {
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-        ParallaxSource.IntProperty var2 = source.addProperty("var2");;
-
-        var1.setIntValue(source, (int)500);
-        var2.setIntValue(source, (int)499);
-
-        source.verifyProperties();
-    }
-
-    @Test(expected=IllegalStateException.class)
-    public void testVerifyIntPropertysWrong_combination() {
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-        ParallaxSource.IntProperty var2 = source.addProperty("var2");
-
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-
-        source.verifyProperties();
-    }
-
-    @Test
-    public void testVerifyIntPropertys_success() {
-        ParallaxSource.IntProperty var1 = source.addProperty("var1");
-        ParallaxSource.IntProperty var2 = source.addProperty("var2");
-
-        var1.setIntValue(source, (int)499);
-        var2.setIntValue(source, (int)500);
-
-        source.verifyProperties();
-
-        var1.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_BEFORE);
-        var2.setIntValue(source, (int)500);
-
-        source.verifyProperties();
-
-        var1.setIntValue(source, (int)499);
-        var2.setIntValue(source, ParallaxSource.IntProperty.UNKNOWN_AFTER);
-
-        source.verifyProperties();
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
new file mode 100644
index 0000000..84fadbd0
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ParallaxIntTest {
+
+    Parallax.IntParallax mSource;
+    int mScreenMax;
+
+    static void assertFloatEquals(float expected, float actual) {
+        org.junit.Assert.assertEquals((double) expected, (double) actual, 0.0001d);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mSource = new Parallax.IntParallax<Parallax.IntProperty>() {
+
+            public int getMaxValue() {
+                return mScreenMax;
+            }
+
+            @Override
+            public IntProperty createProperty(String name, int index) {
+                return new IntProperty(name, index);
+            }
+        };
+    }
+
+    @Test
+    public void testVariable() {
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+        var1.setIntValue(mSource, 54);
+        assertEquals((int) 54, var1.getIntValue(mSource));
+        assertEquals(var1.getName(), "var1");
+        var1.set(mSource, (int) 2000);
+        assertEquals((int) 2000, var1.get(mSource).intValue());
+    }
+
+    @Test
+    public void testFixedKeyValue() {
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+
+        Parallax.IntPropertyMarkerValue keyValue = var1.atAbsolute(1000);
+        assertSame(keyValue.getProperty(), var1);
+        assertEquals((int) 1000, keyValue.getMarkerValue(mSource));
+    }
+
+    @Test
+    public void testFractionOfKeyValue() {
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+
+        Parallax.IntPropertyMarkerValue keyValue = var1.at(0, 0.5f);
+        assertSame(keyValue.getProperty(), var1);
+        assertEquals((int) 540, keyValue.getMarkerValue(mSource));
+    }
+
+    @Test
+    public void testFixedKeyValueWithFraction() {
+        mScreenMax = 1080;
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+
+        Parallax.IntPropertyMarkerValue keyValue = var1.at(-100, 0.5f);
+        assertSame(keyValue.getProperty(), var1);
+        assertEquals((int) 440, keyValue.getMarkerValue(mSource));
+
+        Parallax.IntPropertyMarkerValue keyValue2 = var1.at(100, 0.5f);
+        assertSame(keyValue2.getProperty(), var1);
+        assertEquals((int) 640, keyValue2.getMarkerValue(mSource));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testVerifyIntPropertys_wrongOrder() {
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+        Parallax.IntProperty var2 = mSource.addProperty("var2");
+
+        var1.setIntValue(mSource, (int) 500);
+        var2.setIntValue(mSource, (int) 499);
+
+        mSource.verifyProperties();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testVerifyIntPropertysWrong_combination() {
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+        Parallax.IntProperty var2 = mSource.addProperty("var2");
+
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+
+        mSource.verifyProperties();
+    }
+
+    @Test
+    public void testVerifyIntPropertys_success() {
+        Parallax.IntProperty var1 = mSource.addProperty("var1");
+        Parallax.IntProperty var2 = mSource.addProperty("var2");
+
+        var1.setIntValue(mSource, (int) 499);
+        var2.setIntValue(mSource, (int) 500);
+
+        mSource.verifyProperties();
+
+        var1.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_BEFORE);
+        var2.setIntValue(mSource, (int) 500);
+
+        mSource.verifyProperties();
+
+        var1.setIntValue(mSource, (int) 499);
+        var2.setIntValue(mSource, Parallax.IntProperty.UNKNOWN_AFTER);
+
+        mSource.verifyProperties();
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
index 82261d1..373cb81 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
@@ -16,9 +16,14 @@
 package android.support.v17.leanback.widget;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -30,7 +35,7 @@
 @RunWith(AndroidJUnit4.class)
 public class SingleRowTest extends GridTest {
 
-    SingleRow mSingleRow;
+    private SingleRow mSingleRow;
 
     @Test
     public void testAppendPrependRemove() {
@@ -161,4 +166,97 @@
         mSingleRow.prependVisibleItems(0);
         assertEquals(dump(mSingleRow) + " Should not prepend 0", 1, mSingleRow.mFirstVisibleIndex);
     }
+
+    public void validatePrefetch(int fromLimit, int delta, Integer[]... positionData) {
+        // duplicates logic in support.v7.widget.CacheUtils#verifyPositionsPrefetched
+        RecyclerView.LayoutManager.LayoutPrefetchRegistry registry
+                = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class);
+        mSingleRow.collectAdjacentPrefetchPositions(fromLimit, delta, registry);
+
+        verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt());
+        for (Integer[] aPositionData : positionData) {
+            verify(registry).addPosition(aPositionData[0], aPositionData[1]);
+        }
+    }
+
+    @Test
+    public void testPrefetchBounds() {
+        mProvider = new Provider(new int[]{100, 100});
+
+        mSingleRow = new SingleRow();
+        mSingleRow.setSpacing(20);
+        mSingleRow.setProvider(mProvider);
+        mSingleRow.appendVisibleItems(150);
+
+        validatePrefetch(0, -10);
+        validatePrefetch(-150, 10);
+    }
+
+    @Test
+    public void testPrefetchBoundsReversed() {
+        mProvider = new Provider(new int[]{100, 100});
+
+        mSingleRow = new SingleRow();
+        mSingleRow.setSpacing(20);
+        mSingleRow.setProvider(mProvider);
+        mSingleRow.setReversedFlow(true);
+        mSingleRow.appendVisibleItems(-150);
+
+        validatePrefetch(0, -10);
+        validatePrefetch(150, 10);
+    }
+
+    @Test
+    public void testPrefetchItems() {
+        mProvider = new Provider(new int[]{80, 80, 30, 100, 40, 10});
+
+        mSingleRow = new SingleRow();
+        mSingleRow.setSpacing(20);
+        mSingleRow.setProvider(mProvider);
+        mSingleRow.appendVisibleItems(200);
+
+        // next item, 2, is 0 pixels away
+        validatePrefetch(200, 10, new Integer[] {2, 0});
+
+        // nothing above
+        validatePrefetch(0, -10);
+
+        mProvider.scroll(90);
+        mSingleRow.removeInvisibleItemsAtFront(Integer.MAX_VALUE, 0);
+        mSingleRow.appendVisibleItems(200);
+
+        // next item, 4, is 80 pixels away
+        validatePrefetch(200, 10, new Integer[] {4, 80});
+
+        // next item, 0, is 10 pixels away
+        validatePrefetch(0, -10, new Integer[] {0, 10});
+    }
+
+    @Test
+    public void testPrefetchItemsReversed() {
+        mProvider = new Provider(new int[]{80, 80, 30, 100, 40, 10});
+
+        mSingleRow = new SingleRow();
+        mSingleRow.setSpacing(20);
+        mSingleRow.setProvider(mProvider);
+        mSingleRow.setReversedFlow(true);
+        mSingleRow.appendVisibleItems(-200);
+
+        // next item, 2, is 0 pixels away
+        validatePrefetch(-200, -10, new Integer[] {2, 0});
+
+        // nothing above
+        validatePrefetch(0, 10);
+
+        mProvider.scroll(-90);
+        mSingleRow.removeInvisibleItemsAtFront(Integer.MAX_VALUE, 0);
+        mSingleRow.appendVisibleItems(-200);
+
+        // next item, 4, is 80 pixels away
+        validatePrefetch(-200, -10, new Integer[] {4, 80});
+
+        // one above, 0, is 10 pixels away
+        validatePrefetch(0, 10, new Integer[] {0, 10});
+
+    }
 }
\ No newline at end of file
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
new file mode 100644
index 0000000..11c4d3c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+
+public class TimePickerActivity extends Activity {
+
+    public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
+
+    int mLayoutId;
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mLayoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        setContentView(mLayoutId);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
new file mode 100644
index 0000000..0132e10
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.picker.TimePicker;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TimePickerTest {
+
+    private static final String TAG = "TimePickerTest";
+    private static final long TRANSITION_LENGTH = 1000;
+    private static final long UPDATE_LENGTH = 1000;
+
+
+    Context mContext;
+    View mViewAbove;
+    TimePicker mTimePicker12HourView;
+    TimePicker mTimePicker24HourView;
+    View mViewBelow;
+
+    @Rule
+    public ActivityTestRule<TimePickerActivity> mActivityTestRule =
+            new ActivityTestRule<>(TimePickerActivity.class, false, false);
+    private TimePickerActivity mActivity;
+
+    public void initActivity(Intent intent) throws Throwable {
+        mActivity = mActivityTestRule.launchActivity(intent);
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mTimePicker12HourView = (TimePicker) mActivity.findViewById(R.id.time_picker12);
+        mTimePicker12HourView.setActivatedVisibleItemCount(3);
+        mTimePicker12HourView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mTimePicker12HourView.setActivated(!mTimePicker12HourView.isActivated());
+            }
+        });
+
+        if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets) == R.layout.timepicker_with_other_widgets) {
+            mViewAbove = mActivity.findViewById(R.id.above_picker);
+            mViewBelow = mActivity.findViewById(R.id.below_picker);
+            mTimePicker24HourView = (TimePicker) mActivity.findViewById(R.id.time_picker24);
+            mTimePicker24HourView.setActivatedVisibleItemCount(3);
+            mTimePicker24HourView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mTimePicker24HourView.setActivated(!mTimePicker24HourView.isActivated());
+                }
+            });
+        } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets) == R.layout.timepicker_alone) {
+            // A layout with only a TimePicker widget that is initially activated.
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mTimePicker12HourView.setActivated(true);
+                }
+            });
+            Thread.sleep(500);
+        }
+    }
+
+    @Test
+    public void testSetHourIn24hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(12);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(12));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(13);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(13));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(23);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(23));
+    }
+
+    @Test
+    public void testSetHourIn12hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(12);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(12));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(13);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(13));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(23);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(23));
+    }
+
+    @Test
+    public void testSetMinuteIn24hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(59);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(59));
+    }
+
+    @Test
+    public void testSetMinuteIn12hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(59);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(59));
+
+    }
+
+    @Test
+    public void testAmToPmTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(0);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(0));
+        assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+                mTimePicker12HourView.getMinute(), is(47));
+
+        // traverse to the AM/PM column of 12 hour TimePicker widget
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // scroll down to PM value
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click now to deactivate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(12));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void testPmToAmTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(12);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(12));
+        assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+                mTimePicker12HourView.getMinute(), is(47));
+
+        // traverse to the AM/PM column of 12 hour TimePicker widget
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // scroll down to PM value
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click now to deactivate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(0));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void test12To24HourFormatTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(14);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should be in 12-hour format.", mTimePicker12HourView.is24Hour(),
+                is(false));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setIs24Hour(true);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should now be in 24-hour format.", mTimePicker12HourView.is24Hour(),
+                is(true));
+        // The hour and minute should not be changed.
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(14));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void test24To12HourFormatTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(14);
+                mTimePicker24HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should be in 12-hour format.", mTimePicker24HourView.is24Hour(),
+                is(true));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setIs24Hour(false);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should now be in 24-hour format.", mTimePicker24HourView.is24Hour(),
+                is(false));
+        // The hour and minute should not be changed.
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker24HourView.getHour(), is(14));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker24HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void testInitiallyActiveTimePicker()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_alone);
+        initActivity(intent);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(14);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+
+        ViewGroup mTimePickerInnerView = (ViewGroup) mTimePicker12HourView.findViewById(
+                R.id.picker);
+
+        assertThat("The first column of TimePicker should initially hold focus",
+                mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on first column
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The first column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on second column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of TimePicker should hold focus after scrolling right",
+                mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        // focus on third column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of TimePicker should hold focus after scrolling right",
+                mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+    }
+
+    private void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+}
diff --git a/v17/leanback/tests/res/layout/datepicker_alone.xml b/v17/leanback/tests/res/layout/datepicker_alone.xml
new file mode 100644
index 0000000..79a20b1
--- /dev/null
+++ b/v17/leanback/tests/res/layout/datepicker_alone.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <android.support.v17.leanback.widget.picker.DatePicker
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/date_picker"
+        android:importantForAccessibility="yes"
+        android:focusable="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml b/v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml
new file mode 100644
index 0000000..b5dc630
--- /dev/null
+++ b/v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml
@@ -0,0 +1,52 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <RelativeLayout android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:gravity="center">
+        <TextView
+            android:id="@+id/above_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Above Picker Some Text"
+            android:textAlignment="center"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"
+        />
+        <android.support.v17.leanback.widget.picker.DatePicker
+            android:id="@+id/date_picker"
+            android:importantForAccessibility="yes"
+            android:focusable="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/above_picker"
+            android:gravity="center" />
+        <TextView
+            android:id="@+id/below_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Below Picker Some Text"
+            android:textAlignment="center"
+            android:layout_below="@id/date_picker"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"/>
+    </RelativeLayout>
+</RelativeLayout>
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
new file mode 100644
index 0000000..bb11772
--- /dev/null
+++ b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
@@ -0,0 +1,41 @@
+<!--
+  ~ 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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:lb="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layoutDirection="rtl"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+  <android.support.v17.leanback.widget.HorizontalGridViewEx
+      android:id="@+id/gridview"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:clipToPadding="false"
+      android:focusable="true"
+      android:focusableInTouchMode="true"
+      android:background="#00ffff"
+      android:horizontalSpacing="12dip"
+      android:verticalSpacing="24dip"
+      lb:numberOfColumns="1"
+      lb:columnWidth="150dip"
+      android:paddingBottom="12dip"
+      android:paddingLeft="12dip"
+      android:paddingRight="12dip"
+      android:paddingTop="12dip" />
+</LinearLayout>
diff --git a/v17/leanback/tests/res/layout/playback_controls.xml b/v17/leanback/tests/res/layout/playback_controls.xml
deleted file mode 100644
index 7f8910f..0000000
--- a/v17/leanback/tests/res/layout/playback_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent" >
-
-    <fragment
-            android:id="@+id/playback_controls_fragment"
-            android:name="android.support.v17.leanback.app.PlaybackTestFragment"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-
-</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_controls_with_video.xml b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
index cbf2a91..1850638 100644
--- a/v17/leanback/tests/res/layout/playback_controls_with_video.xml
+++ b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
@@ -26,7 +26,7 @@
         android:layout_gravity="center" />
 
     <fragment
-        android:id="@+id/playback_controls_fragment"
+        android:id="@+id/main_frame"
         android:name="android.support.v17.leanback.app.PlaybackOverlayTestFragment"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
diff --git a/v17/leanback/tests/res/layout/playback_support_controls.xml b/v17/leanback/tests/res/layout/playback_support_controls.xml
deleted file mode 100644
index 9e0e092..0000000
--- a/v17/leanback/tests/res/layout/playback_support_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent" >
-
-    <fragment
-            android:id="@+id/playback_controls_fragment"
-            android:name="android.support.v17.leanback.app.PlaybackSupportTestFragment"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-
-</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/rows.xml b/v17/leanback/tests/res/layout/single_fragment.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/rows.xml
rename to v17/leanback/tests/res/layout/single_fragment.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_alone.xml b/v17/leanback/tests/res/layout/timepicker_alone.xml
new file mode 100644
index 0000000..2ca4484
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_alone.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <android.support.v17.leanback.widget.picker.TimePicker
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/time_picker12"
+        android:importantForAccessibility="yes"
+        app:is24HourFormat="false"
+        app:useCurrentTime="true"
+        android:focusable="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
new file mode 100644
index 0000000..677442a
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
@@ -0,0 +1,65 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <RelativeLayout android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:gravity="center">
+        <TextView
+            android:id="@+id/above_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Above Picker Some Text"
+            android:textAlignment="center"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"
+        />
+        <android.support.v17.leanback.widget.picker.TimePicker
+            android:id="@+id/time_picker12"
+            android:importantForAccessibility="yes"
+            app:is24HourFormat="false"
+            app:useCurrentTime="true"
+            android:focusable="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/above_picker"
+            android:gravity="center" />
+        <android.support.v17.leanback.widget.picker.TimePicker
+            android:id="@+id/time_picker24"
+            android:importantForAccessibility="yes"
+            app:is24HourFormat="true"
+            app:useCurrentTime="true"
+            android:focusable="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/time_picker12"
+            android:gravity="center" />
+        <TextView
+            android:id="@+id/below_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Below Picker Some Text"
+            android:textAlignment="center"
+            android:layout_below="@id/time_picker24"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"/>
+    </RelativeLayout>
+</RelativeLayout>
diff --git a/v17/leanback/tests/res/raw/track_01.mp3 b/v17/leanback/tests/res/raw/track_01.mp3
new file mode 100755
index 0000000..9762383
--- /dev/null
+++ b/v17/leanback/tests/res/raw/track_01.mp3
Binary files differ
diff --git a/v17/leanback/tests/res/raw/video.mp4 b/v17/leanback/tests/res/raw/video.mp4
new file mode 100644
index 0000000..3f709fb
--- /dev/null
+++ b/v17/leanback/tests/res/raw/video.mp4
Binary files differ
diff --git a/v17/preference-leanback/Android.mk b/v17/preference-leanback/Android.mk
index 263d334..8c0488f 100644
--- a/v17/preference-leanback/Android.mk
+++ b/v17/preference-leanback/Android.mk
@@ -35,6 +35,7 @@
     $(call all-java-files-under,api21) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v17-leanback \
     android-support-v14-preference \
diff --git a/v17/preference-leanback/AndroidManifest-make.xml b/v17/preference-leanback/AndroidManifest-make.xml
new file mode 100644
index 0000000..e2cfe35
--- /dev/null
+++ b/v17/preference-leanback/AndroidManifest-make.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.support.v17.preference"
+    android:versionCode="1"
+    android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="17" />
+    <application />
+</manifest>
diff --git a/v17/preference-leanback/build.gradle b/v17/preference-leanback/build.gradle
index 9bfd0f3..e58fa8b 100644
--- a/v17/preference-leanback/build.gradle
+++ b/v17/preference-leanback/build.gradle
@@ -36,28 +36,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v4/Android.mk b/v4/Android.mk
index a9c9145..c7e35aa 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -35,6 +35,7 @@
     android-support-fragment \
     android-support-annotations
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
diff --git a/v4/AndroidManifest-make.xml b/v4/AndroidManifest-make.xml
new file mode 100644
index 0000000..d76c581
--- /dev/null
+++ b/v4/AndroidManifest-make.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.v4">
+    <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.v4"/>
+    <application />
+</manifest>
diff --git a/v4/AndroidManifest.xml b/v4/AndroidManifest.xml
index d76c581..cecc743 100644
--- a/v4/AndroidManifest.xml
+++ b/v4/AndroidManifest.xml
@@ -17,5 +17,6 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="android.support.v4">
     <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.v4"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v4/build.gradle b/v4/build.gradle
index f0226e3..f226a87 100644
--- a/v4/build.gradle
+++ b/v4/build.gradle
@@ -1,6 +1,6 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'support-v4'
+
 dependencies {
     compile project(':support-compat')
     compile project(':support-media-compat')
diff --git a/v7/appcompat/Android.mk b/v7/appcompat/Android.mk
index 93baa95..3c5b44c 100644
--- a/v7/appcompat/Android.mk
+++ b/v7/appcompat/Android.mk
@@ -28,6 +28,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-vectordrawable \
     android-support-animatedvectordrawable
diff --git a/v7/appcompat/AndroidManifest-make.xml b/v7/appcompat/AndroidManifest-make.xml
new file mode 100644
index 0000000..99b77ee
--- /dev/null
+++ b/v7/appcompat/AndroidManifest-make.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.v7.appcompat">
+    <uses-sdk android:minSdkVersion="9"
+              tools:overrideLibrary="android.support.graphics.drawable.animated"/>
+    <application/>
+</manifest>
diff --git a/v7/appcompat/AndroidManifest.xml b/v7/appcompat/AndroidManifest.xml
index 99b77ee..d5858d1 100644
--- a/v7/appcompat/AndroidManifest.xml
+++ b/v7/appcompat/AndroidManifest.xml
@@ -18,5 +18,6 @@
           package="android.support.v7.appcompat">
     <uses-sdk android:minSdkVersion="9"
               tools:overrideLibrary="android.support.graphics.drawable.animated"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application/>
 </manifest>
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index aa62632..4935085 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'appcompat-v7'
 
 dependencies {
@@ -61,28 +60,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/appcompat/res/layout/abc_action_mode_close_item_material.xml b/v7/appcompat/res/layout/abc_action_mode_close_item_material.xml
index b3babb2..118ce2b 100644
--- a/v7/appcompat/res/layout/abc_action_mode_close_item_material.xml
+++ b/v7/appcompat/res/layout/abc_action_mode_close_item_material.xml
@@ -14,14 +14,17 @@
      limitations under the License.
 -->
 
-<ImageView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:app="http://schemas.android.com/apk/res-auto"
-        android:id="@+id/action_mode_close_button"
-        android:contentDescription="@string/abc_action_mode_done"
-        android:focusable="true"
-        android:clickable="true"
-        app:srcCompat="?attr/actionModeCloseDrawable"
-        style="?attr/actionModeCloseButtonStyle"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"/>
\ No newline at end of file
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/action_mode_close_button"
+    style="?attr/actionModeCloseButtonStyle"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:layout_marginEnd="16dip"
+    android:layout_marginRight="16dip"
+    android:clickable="true"
+    android:contentDescription="@string/abc_action_mode_done"
+    android:focusable="true"
+    android:paddingLeft="8dp"
+    android:paddingStart="8dp"
+    app:srcCompat="?attr/actionModeCloseDrawable" />
\ No newline at end of file
diff --git a/v7/appcompat/res/values-v11/themes_base.xml b/v7/appcompat/res/values-v11/themes_base.xml
index d38ef32..11ef632 100644
--- a/v7/appcompat/res/values-v11/themes_base.xml
+++ b/v7/appcompat/res/values-v11/themes_base.xml
@@ -32,6 +32,7 @@
 
         <item name="android:buttonBarStyle">?attr/buttonBarStyle</item>
         <item name="android:buttonBarButtonStyle">?attr/buttonBarButtonStyle</item>
+        <item name="android:borderlessButtonStyle">?attr/borderlessButtonStyle</item>
 
         <!-- Window colors -->
         <item name="android:colorForeground">@color/foreground_material_dark</item>
@@ -86,6 +87,7 @@
 
         <item name="android:buttonBarStyle">?attr/buttonBarStyle</item>
         <item name="android:buttonBarButtonStyle">?attr/buttonBarButtonStyle</item>
+        <item name="android:borderlessButtonStyle">?attr/borderlessButtonStyle</item>
 
         <!-- Window colors -->
         <item name="android:colorForeground">@color/foreground_material_light</item>
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarDrawerToggle.java b/v7/appcompat/src/android/support/v7/app/ActionBarDrawerToggle.java
index 1fe51b8..cbfb397 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarDrawerToggle.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarDrawerToggle.java
@@ -118,6 +118,7 @@
     private final DrawerLayout mDrawerLayout;
 
     private DrawerArrowDrawable mSlider;
+    private boolean mDrawerSlideAnimationEnabled = true;
     private Drawable mHomeAsUpIndicator;
     boolean mDrawerIndicatorEnabled = true;
     private boolean mHasCustomUpIndicator;
@@ -380,7 +381,7 @@
     /**
      * Sets the DrawerArrowDrawable that should be shown by this ActionBarDrawerToggle.
      *
-     * @param drawable DrawerArrowDrawable that should be shown by this ActionBarDrawerToggle.
+     * @param drawable DrawerArrowDrawable that should be shown by this ActionBarDrawerToggle
      */
     public void setDrawerArrowDrawable(@NonNull DrawerArrowDrawable drawable) {
         mSlider = drawable;
@@ -388,6 +389,25 @@
     }
 
     /**
+     * Specifies whether the drawer arrow should animate when the drawer position changes.
+     *
+     * @param enabled if this is {@code true} then the animation will run, else it will be skipped
+     */
+    public void setDrawerSlideAnimationEnabled(boolean enabled) {
+        mDrawerSlideAnimationEnabled = enabled;
+        if (!enabled) {
+            setPosition(0);
+        }
+    }
+
+    /**
+     * @return whether the drawer slide animation is enabled
+     */
+    public boolean isDrawerSlideAnimationEnabled() {
+        return mDrawerSlideAnimationEnabled;
+    }
+
+    /**
      * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
      * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
      * through to this method from your own listener object.
@@ -397,7 +417,11 @@
      */
     @Override
     public void onDrawerSlide(View drawerView, float slideOffset) {
-        setPosition(Math.min(1f, Math.max(0, slideOffset)));
+        if (mDrawerSlideAnimationEnabled) {
+            setPosition(Math.min(1f, Math.max(0, slideOffset)));
+        } else {
+            setPosition(0); // disable animation.
+        }
     }
 
     /**
diff --git a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV9.java b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV9.java
index b52c0ba..c5839f9 100644
--- a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV9.java
+++ b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV9.java
@@ -92,11 +92,15 @@
 import android.widget.PopupWindow;
 import android.widget.TextView;
 
+import org.xmlpull.v1.XmlPullParser;
+
 @RequiresApi(9)
 @TargetApi(9)
 class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
         implements MenuBuilder.Callback, LayoutInflaterFactory {
 
+    private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
+
     private DecorContentParent mDecorContentParent;
     private ActionMenuPresenterCallback mActionMenuPresenterCallback;
     private PanelMenuPresenterCallback mPanelMenuPresenterCallback;
@@ -1009,17 +1013,21 @@
     @Override
     public View createView(View parent, final String name, @NonNull Context context,
             @NonNull AttributeSet attrs) {
-        final boolean isPre21 = Build.VERSION.SDK_INT < 21;
-
         if (mAppCompatViewInflater == null) {
             mAppCompatViewInflater = new AppCompatViewInflater();
         }
 
-        // We only want the View to inherit its context if we're running pre-v21
-        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
+        boolean inheritContext = false;
+        if (IS_PRE_LOLLIPOP) {
+            inheritContext = (attrs instanceof XmlPullParser)
+                    // If we have a XmlPullParser, we can detect where we are in the layout
+                    ? ((XmlPullParser) attrs).getDepth() > 1
+                    // Otherwise we have to use the old heuristic
+                    : shouldInheritContext((ViewParent) parent);
+        }
 
         return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
-                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
+                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                 true, /* Read read app:theme as a fallback at all times for legacy reasons */
                 VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
         );
@@ -1068,8 +1076,7 @@
      * From {@link android.support.v4.view.LayoutInflaterFactory}
      */
     @Override
-    public final View onCreateView(View parent, String name,
-            Context context, AttributeSet attrs) {
+    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
         // First let the Activity's Factory try and inflate the view
         final View view = callActivityOnCreateView(parent, name, context, attrs);
         if (view != null) {
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index bd72879..a744c5f 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -656,7 +656,7 @@
      * Notification noti = new NotificationCompat.Builder()
      *     .setSmallIcon(R.drawable.ic_stat_player)
      *     .setLargeIcon(albumArtBitmap))
-     *     .setCustomContentView(contentView);
+     *     .setCustomContentView(contentView)
      *     .setStyle(<b>new NotificationCompat.DecoratedCustomViewStyle()</b>)
      *     .build();
      * </pre>
@@ -690,7 +690,7 @@
      * Notification noti = new Notification.Builder()
      *     .setSmallIcon(R.drawable.ic_stat_player)
      *     .setLargeIcon(albumArtBitmap))
-     *     .setCustomContentView(contentView);
+     *     .setCustomContentView(contentView)
      *     .setStyle(<b>new NotificationCompat.DecoratedMediaCustomViewStyle()</b>
      *          .setMediaSession(mySession))
      *     .build();
diff --git a/v7/appcompat/src/android/support/v7/view/menu/CascadingMenuPopup.java b/v7/appcompat/src/android/support/v7/view/menu/CascadingMenuPopup.java
index b62127e..d3c2fc8 100644
--- a/v7/appcompat/src/android/support/v7/view/menu/CascadingMenuPopup.java
+++ b/v7/appcompat/src/android/support/v7/view/menu/CascadingMenuPopup.java
@@ -27,6 +27,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.StyleRes;
+import android.support.v4.internal.view.SupportMenu;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.appcompat.R;
@@ -163,7 +164,7 @@
 
                     // Then open the selected submenu, if there is one.
                     if (item.isEnabled() && item.hasSubMenu()) {
-                        menu.performItemAction(item, 0);
+                        menu.performItemAction(item, SupportMenu.FLAG_KEEP_OPEN_ON_SUBMENU_OPENED);
                     }
                 }
             };
diff --git a/v7/appcompat/src/android/support/v7/widget/SwitchCompat.java b/v7/appcompat/src/android/support/v7/widget/SwitchCompat.java
index c9e1dc8..3ccf763 100644
--- a/v7/appcompat/src/android/support/v7/widget/SwitchCompat.java
+++ b/v7/appcompat/src/android/support/v7/widget/SwitchCompat.java
@@ -16,10 +16,11 @@
 
 package android.support.v7.widget;
 
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
@@ -29,6 +30,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
 import android.support.v4.graphics.drawable.DrawableCompat;
 import android.support.v4.view.MotionEventCompat;
 import android.support.v4.view.ViewCompat;
@@ -41,6 +43,7 @@
 import android.text.TextUtils;
 import android.text.method.TransformationMethod;
 import android.util.AttributeSet;
+import android.util.Property;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.SoundEffectConstants;
@@ -48,8 +51,6 @@
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
 import android.widget.CompoundButton;
 
 /**
@@ -80,6 +81,8 @@
  * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTextPadding
  * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_track
  */
+@RequiresApi(14)
+@TargetApi(14)
 public class SwitchCompat extends CompoundButton {
     private static final int THUMB_ANIMATION_DURATION = 250;
 
@@ -96,6 +99,19 @@
     private static final int SERIF = 2;
     private static final int MONOSPACE = 3;
 
+    private static final Property<SwitchCompat, Float> THUMB_POS =
+            new Property<SwitchCompat, Float>(Float.class, "thumbPos") {
+                @Override
+                public Float get(SwitchCompat object) {
+                    return object.mThumbPosition;
+                }
+
+                @Override
+                public void set(SwitchCompat object, Float value) {
+                    object.setThumbPosition(value);
+                }
+            };
+
     private Drawable mThumbDrawable;
     private ColorStateList mThumbTintList = null;
     private PorterDuff.Mode mThumbTintMode = null;
@@ -160,7 +176,7 @@
     private Layout mOnLayout;
     private Layout mOffLayout;
     private TransformationMethod mSwitchTransformationMethod;
-    ThumbAnimation mPositionAnimator;
+    ObjectAnimator mPositionAnimator;
 
     @SuppressWarnings("hiding")
     private final Rect mTempRect = new Rect();
@@ -1005,36 +1021,18 @@
     }
 
     private void animateThumbToCheckedState(final boolean newCheckedState) {
-        if (mPositionAnimator != null) {
-            // If there's a current animator running, cancel it
-            cancelPositionAnimator();
-        }
-
-        mPositionAnimator = new ThumbAnimation(mThumbPosition, newCheckedState ? 1f : 0f);
+        final float targetPosition = newCheckedState ? 1 : 0;
+        mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
         mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
-        mPositionAnimator.setAnimationListener(new Animation.AnimationListener() {
-            @Override
-            public void onAnimationStart(Animation animation) {}
-
-            @Override
-            public void onAnimationEnd(Animation animation) {
-                if (mPositionAnimator == animation) {
-                    // If we're still the active animation, ensure the final position
-                    setThumbPosition(newCheckedState ? 1f : 0f);
-                    mPositionAnimator = null;
-                }
-            }
-
-            @Override
-            public void onAnimationRepeat(Animation animation) {}
-        });
-        startAnimation(mPositionAnimator);
+        if (Build.VERSION.SDK_INT >= 18) {
+            mPositionAnimator.setAutoCancel(true);
+        }
+        mPositionAnimator.start();
     }
 
     private void cancelPositionAnimator() {
         if (mPositionAnimator != null) {
-            clearAnimation();
-            mPositionAnimator = null;
+            mPositionAnimator.cancel();
         }
     }
 
@@ -1065,7 +1063,7 @@
         // recursively with a different value, so load the REAL value...
         checked = isChecked();
 
-        if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) {
+        if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
             animateThumbToCheckedState(checked);
         } else {
             // Immediately move the thumb to the new position.
@@ -1372,7 +1370,7 @@
 
     @Override
     public void jumpDrawablesToCurrentState() {
-        if (Build.VERSION.SDK_INT >= 11) {
+        if (Build.VERSION.SDK_INT >= 14) {
             super.jumpDrawablesToCurrentState();
 
             if (mThumbDrawable != null) {
@@ -1383,8 +1381,10 @@
                 mTrackDrawable.jumpToCurrentState();
             }
 
-            cancelPositionAnimator();
-            setThumbPosition(isChecked() ? 1 : 0);
+            if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
+                mPositionAnimator.end();
+                mPositionAnimator = null;
+            }
         }
     }
 
@@ -1419,21 +1419,4 @@
     private static float constrain(float amount, float low, float high) {
         return amount < low ? low : (amount > high ? high : amount);
     }
-
-    private class ThumbAnimation extends Animation {
-        final float mStartPosition;
-        final float mEndPosition;
-        final float mDiff;
-
-        ThumbAnimation(float startPosition, float endPosition) {
-            mStartPosition = startPosition;
-            mEndPosition = endPosition;
-            mDiff = endPosition - startPosition;
-        }
-
-        @Override
-        protected void applyTransformation(float interpolatedTime, Transformation t) {
-            setThumbPosition(mStartPosition + (mDiff * interpolatedTime));
-        }
-    }
 }
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/layout/layout_children.xml b/v7/appcompat/tests/res/layout/layout_children.xml
new file mode 100644
index 0000000..a61175a
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/layout_children.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:orientation="vertical">
+
+    <TextView android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:text="Test"/>
+
+    <TextView android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:text="Test"/>
+
+    <TextView android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:text="Test"/>
+
+    <TextView android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:text="Test"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogCursorTest.java b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogCursorTest.java
index 0d488ab..8afd231 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogCursorTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogCursorTest.java
@@ -143,6 +143,9 @@
         if (mDatabaseFile != null) {
             mDatabaseFile.delete();
         }
+        if (mAlertDialog != null) {
+            mAlertDialog.dismiss();
+        }
     }
 
     private void wireBuilder(final AlertDialog.Builder builder) {
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
index b37347d..022face 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
@@ -74,6 +74,7 @@
 import android.widget.ListView;
 
 import org.hamcrest.Matcher;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -109,6 +110,13 @@
         mButton = (Button) activity.findViewById(R.id.test_button);
     }
 
+    @After
+    public void tearDown() {
+        if (mAlertDialog != null) {
+            mAlertDialog.dismiss();
+        }
+    }
+
     private void wireBuilder(final AlertDialog.Builder builder) {
         mButton.setOnClickListener(new View.OnClickListener() {
             @Override
diff --git a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
index d435f42..844f527 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
@@ -29,7 +29,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
 import android.support.v7.appcompat.test.R;
 import android.support.v7.testutils.BaseTestActivity;
 import android.support.v7.view.ActionMode;
@@ -89,8 +92,10 @@
         assertTrue("ActionMode was destroyed", destroyed.get());
     }
 
+    @Suppress
+    @FlakyTest(bugId = 34956766)
     @Test
-    @SmallTest
+    @LargeTest
     public void testBackCollapsesSearchView() throws InterruptedException {
         final String itemTitle = getActivity().getString(R.string.search_menu_title);
 
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
index 3fe899d..d81856c 100755
--- a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
@@ -44,9 +44,11 @@
 import android.support.test.espresso.action.GeneralSwipeAction;
 import android.support.test.espresso.action.Press;
 import android.support.test.espresso.action.Swipe;
+import android.support.test.filters.FlakyTest;
 import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
 import android.support.v7.appcompat.test.R;
@@ -426,6 +428,8 @@
         mDrawerLayout.removeDrawerListener(mockedListener);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 33659300)
     @Test
     @SmallTest
     public void testDrawerListenerCallbacksOnOpeningViaSwipes() {
diff --git a/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
index f7004b3..0502ad4 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
@@ -37,9 +37,11 @@
 import android.support.v7.widget.AppCompatRatingBar;
 import android.support.v7.widget.AppCompatSpinner;
 import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.LinearLayout;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -82,13 +84,28 @@
         LayoutInflater inflater = LayoutInflater.from(getActivity());
         final ViewGroup root = (ViewGroup) inflater.inflate(
                 R.layout.layout_android_theme_children, null);
-
         assertThemedContext(root);
+    }
 
-        for (int i = 0; i < root.getChildCount(); i++) {
-            final View child = root.getChildAt(i);
-            assertThemedContext(child);
-        }
+    @UiThreadTest
+    @Test
+    @SmallTest
+    public void testThemedInflationWithUnattachedParent() {
+        final Context activity = getActivity();
+
+        // Create a parent but not attached
+        final LinearLayout parent = new LinearLayout(activity);
+
+        // Now create a LayoutInflater with a themed context
+        LayoutInflater inflater = LayoutInflater.from(activity)
+                .cloneInContext(new ContextThemeWrapper(activity, R.style.MagentaThemeOverlay));
+
+        // Now inflate a layout with children
+        final ViewGroup root = (ViewGroup) inflater.inflate(
+                R.layout.layout_children, parent, false);
+
+        // And assert that the layout is themed correctly
+        assertThemedContext(root);
     }
 
     @UiThreadTest
@@ -190,7 +207,7 @@
                 view.getClass());
     }
 
-    private static void assertThemedContext(View view) {
+    private static void assertThemedContext(final View view) {
         final Context viewContext = view.getContext();
         final int expectedColor = view.getResources().getColor(R.color.test_magenta);
 
@@ -199,6 +216,14 @@
                 && colorAccentValue.type <= TypedValue.TYPE_LAST_COLOR_INT);
         assertEquals("View does not have ContextThemeWrapper context",
                 expectedColor, colorAccentValue.data);
+
+        if (view instanceof ViewGroup) {
+            ViewGroup vg = (ViewGroup) view;
+            for (int i = 0; i < vg.getChildCount(); i++) {
+                final View child = vg.getChildAt(i);
+                assertThemedContext(child);
+            }
+        }
     }
 
     private static TypedValue getColorAccentValue(final Resources.Theme theme) {
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
index 44c05ae..34c07a3 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
@@ -43,6 +43,8 @@
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
 import android.support.v7.app.BaseInstrumentationTestCase;
@@ -125,8 +127,9 @@
                 .check(matches(isDisplayed()));
     }
 
+    @FlakyTest(bugId = 33669575)
     @Test
-    @SmallTest
+    @LargeTest
     public void testAnchoring() {
         Builder popupBuilder = new Builder();
         popupBuilder.wireToActionButton();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
index 3cf511f..7d6e39f 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
@@ -48,6 +48,8 @@
 import android.support.test.espresso.Root;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.v7.app.BaseInstrumentationTestCase;
 import android.support.v7.appcompat.test.R;
@@ -260,8 +262,9 @@
         };
     }
 
+    @FlakyTest(bugId = 33669575)
     @Test
-    @MediumTest
+    @LargeTest
     public void testAnchoring() {
         Builder menuBuilder = new Builder();
         menuBuilder.wireToActionButton();
diff --git a/v7/cardview/Android.mk b/v7/cardview/Android.mk
index cd3b407..40f6b23 100644
--- a/v7/cardview/Android.mk
+++ b/v7/cardview/Android.mk
@@ -31,6 +31,7 @@
     $(call all-java-files-under,api21) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations
 LOCAL_JAR_EXCLUDE_FILES := none
diff --git a/v7/cardview/AndroidManifest-make.xml b/v7/cardview/AndroidManifest-make.xml
new file mode 100644
index 0000000..c35e369
--- /dev/null
+++ b/v7/cardview/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v7.cardview">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/v7/cardview/AndroidManifest.xml b/v7/cardview/AndroidManifest.xml
index c35e369..e85003c 100644
--- a/v7/cardview/AndroidManifest.xml
+++ b/v7/cardview/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v7.cardview">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v7/cardview/build.gradle b/v7/cardview/build.gradle
index b88aad7..ce3f28d 100644
--- a/v7/cardview/build.gradle
+++ b/v7/cardview/build.gradle
@@ -38,28 +38,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/gridlayout/Android.mk b/v7/gridlayout/Android.mk
index 6eac23b4..c584dbe 100644
--- a/v7/gridlayout/Android.mk
+++ b/v7/gridlayout/Android.mk
@@ -29,6 +29,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-core-ui \
diff --git a/v7/gridlayout/AndroidManifest-make.xml b/v7/gridlayout/AndroidManifest-make.xml
new file mode 100644
index 0000000..d2cc627
--- /dev/null
+++ b/v7/gridlayout/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v7.gridlayout">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/v7/gridlayout/AndroidManifest.xml b/v7/gridlayout/AndroidManifest.xml
index d2cc627..dfcc942 100644
--- a/v7/gridlayout/AndroidManifest.xml
+++ b/v7/gridlayout/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v7.gridlayout">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v7/gridlayout/build.gradle b/v7/gridlayout/build.gradle
index 542d5a7..56f320f 100644
--- a/v7/gridlayout/build.gradle
+++ b/v7/gridlayout/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'gridlayout-v7'
 
 dependencies {
@@ -53,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/mediarouter/Android.mk b/v7/mediarouter/Android.mk
index 21b4a62..6162f879 100644
--- a/v7/mediarouter/Android.mk
+++ b/v7/mediarouter/Android.mk
@@ -35,6 +35,7 @@
     $(call all-java-files-under,api24) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v7-appcompat \
     android-support-v7-palette \
diff --git a/v7/mediarouter/AndroidManifest-make.xml b/v7/mediarouter/AndroidManifest-make.xml
new file mode 100644
index 0000000..59d9f99
--- /dev/null
+++ b/v7/mediarouter/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v7.mediarouter">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/v7/mediarouter/AndroidManifest.xml b/v7/mediarouter/AndroidManifest.xml
index 59d9f99..bbebdfed 100644
--- a/v7/mediarouter/AndroidManifest.xml
+++ b/v7/mediarouter/AndroidManifest.xml
@@ -16,5 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v7.mediarouter">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index 3e51602..7796565 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -49,28 +49,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_stop_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..801d341
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_stop_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..9d6b65d
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_stop_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_stop_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..3ad2c9c
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_stop_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..b002ab7
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_stop_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..5239336
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..5bc5a6c
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_stop_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_dark.png b/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..035ca18
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_light.png b/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..eac183d
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xxhdpi/ic_media_stop_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable/mr_media_stop_dark.xml b/v7/mediarouter/res/drawable/mr_media_stop_dark.xml
new file mode 100644
index 0000000..9757552
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_media_stop_dark.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/ic_media_stop_dark" />
+</selector>
diff --git a/v7/mediarouter/res/drawable/mr_media_stop_light.xml b/v7/mediarouter/res/drawable/mr_media_stop_light.xml
new file mode 100644
index 0000000..c7717c7
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_media_stop_light.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <bitmap
+            android:src="@drawable/ic_media_stop_light"
+            android:alpha="0.87" />
+    </item>
+</selector>
diff --git a/v7/mediarouter/res/layout/mr_playback_control.xml b/v7/mediarouter/res/layout/mr_playback_control.xml
index b441254..9d788f6 100644
--- a/v7/mediarouter/res/layout/mr_playback_control.xml
+++ b/v7/mediarouter/res/layout/mr_playback_control.xml
@@ -20,7 +20,7 @@
                 android:orientation="horizontal"
                 android:paddingLeft="24dp"
                 android:paddingRight="12dp" >
-    <ImageButton android:id="@+id/mr_control_play_pause"
+    <ImageButton android:id="@+id/mr_control_playback_ctrl"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_marginLeft="12dp"
@@ -33,7 +33,7 @@
                   android:orientation="vertical"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
-                  android:layout_toLeftOf="@id/mr_control_play_pause"
+                  android:layout_toLeftOf="@id/mr_control_playback_ctrl"
                   android:layout_alignParentLeft="true"
                   android:layout_centerVertical="true">
         <TextView android:id="@+id/mr_control_title"
diff --git a/v7/mediarouter/res/values-af/strings.xml b/v7/mediarouter/res/values-af/strings.xml
index 9811194..d816976 100644
--- a/v7/mediarouter/res/values-af/strings.xml
+++ b/v7/mediarouter/res/values-af/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Saai uit na"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Vind tans toestelle"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ontkoppel"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Hou op uitsaai"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Hou op uitsaai"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Maak toe"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Speel"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Laat wag"</string>
diff --git a/v7/mediarouter/res/values-am/strings.xml b/v7/mediarouter/res/values-am/strings.xml
index 6f7931d..d9ee6b7 100644
--- a/v7/mediarouter/res/values-am/strings.xml
+++ b/v7/mediarouter/res/values-am/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast አድርግ ወደ"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"መሣሪያዎችን በማግኘት ላይ"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ግንኙነት አቋርጥ"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Cast ማድረግ አቁም"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Cast ማድረግ አቁም"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ዝጋ"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"አጫውት"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ለአፍታ አቁም"</string>
diff --git a/v7/mediarouter/res/values-ar/strings.xml b/v7/mediarouter/res/values-ar/strings.xml
index 29cab47..6365f01 100644
--- a/v7/mediarouter/res/values-ar/strings.xml
+++ b/v7/mediarouter/res/values-ar/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"إرسال إلى"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"جارٍ البحث عن أجهزة"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"قطع الاتصال"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"إيقاف الإرسال"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"إيقاف الإرسال"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"إغلاق"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"تشغيل"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"إيقاف مؤقت"</string>
diff --git a/v7/mediarouter/res/values-az-rAZ/strings.xml b/v7/mediarouter/res/values-az-rAZ/strings.xml
index 765520e..59bd17d 100644
--- a/v7/mediarouter/res/values-az-rAZ/strings.xml
+++ b/v7/mediarouter/res/values-az-rAZ/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Bura yayımlayın"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Cihazlar axtarılır"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Bağlantını kəsin"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Yayımı dayandırın"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Yayımı dayandırın"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Qapadın"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Oynadın"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Durdurun"</string>
diff --git a/v7/mediarouter/res/values-b+sr+Latn/strings.xml b/v7/mediarouter/res/values-b+sr+Latn/strings.xml
index 8075d2e..e64356e 100644
--- a/v7/mediarouter/res/values-b+sr+Latn/strings.xml
+++ b/v7/mediarouter/res/values-b+sr+Latn/strings.xml
@@ -25,7 +25,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Prebacujte na"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Pronalaženje uređaja"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zaustavi prebacivanje"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zaustavi prebacivanje"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvori"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Pusti"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziraj"</string>
diff --git a/v7/mediarouter/res/values-be-rBY/strings.xml b/v7/mediarouter/res/values-be-rBY/strings.xml
index 75c24d5..93ccc1a 100644
--- a/v7/mediarouter/res/values-be-rBY/strings.xml
+++ b/v7/mediarouter/res/values-be-rBY/strings.xml
@@ -25,7 +25,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Трансляваць на"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Пошук прылад"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Адлучыць"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Спыніць трансляцыю"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Спыніць трансляцыю"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрыць"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Прайграць"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Прыпыніць"</string>
diff --git a/v7/mediarouter/res/values-bg/strings.xml b/v7/mediarouter/res/values-bg/strings.xml
index 036b31f..54f3d29 100644
--- a/v7/mediarouter/res/values-bg/strings.xml
+++ b/v7/mediarouter/res/values-bg/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Предаване към"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Търсят се устройства"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Прекратяване на връзката"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Спиране на предаването"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Спиране на предаването"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Затваряне"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Пускане"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Поставяне на пауза"</string>
diff --git a/v7/mediarouter/res/values-bn-rBD/strings.xml b/v7/mediarouter/res/values-bn-rBD/strings.xml
index 0e3e491..9640eab 100644
--- a/v7/mediarouter/res/values-bn-rBD/strings.xml
+++ b/v7/mediarouter/res/values-bn-rBD/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"এতে কাস্ট করুন"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ডিভাইসগুলিকে খোঁজা হচ্ছে"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"সংযোগ বিচ্ছিন্ন করুন"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"কাস্ট করা বন্ধ করুন"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"কাস্ট করা বন্ধ করুন"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"বন্ধ করুন"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"চালান"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"বিরাম দিন"</string>
diff --git a/v7/mediarouter/res/values-bs-rBA/strings.xml b/v7/mediarouter/res/values-bs-rBA/strings.xml
index df4e3ef..cd8e586 100644
--- a/v7/mediarouter/res/values-bs-rBA/strings.xml
+++ b/v7/mediarouter/res/values-bs-rBA/strings.xml
@@ -25,7 +25,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Prebacujte na"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Traženje uređaja"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zaustavi prebacivanje"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zaustavi prebacivanje"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvori"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproduciraj"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziraj"</string>
diff --git a/v7/mediarouter/res/values-ca/strings.xml b/v7/mediarouter/res/values-ca/strings.xml
index 7fc51f7..c7b413a 100644
--- a/v7/mediarouter/res/values-ca/strings.xml
+++ b/v7/mediarouter/res/values-ca/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Emet a"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"S\'estan cercant dispositius"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconnecta"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Atura l\'emissió"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Atura l\'emissió"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Tanca"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reprodueix"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Posa en pausa"</string>
diff --git a/v7/mediarouter/res/values-cs/strings.xml b/v7/mediarouter/res/values-cs/strings.xml
index f5a286e..624aecb 100644
--- a/v7/mediarouter/res/values-cs/strings.xml
+++ b/v7/mediarouter/res/values-cs/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Odesílat do"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Hledání zařízení"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odpojit"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zastavit odesílání"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zastavit odesílání"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zavřít"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Přehrát"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pozastavit"</string>
diff --git a/v7/mediarouter/res/values-da/strings.xml b/v7/mediarouter/res/values-da/strings.xml
index 9e5f9a5..7b93dd1 100644
--- a/v7/mediarouter/res/values-da/strings.xml
+++ b/v7/mediarouter/res/values-da/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast til"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Finder enheder"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Afbryd"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stop med at caste"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stop med at caste"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Luk"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Afspil"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Sæt på pause"</string>
diff --git a/v7/mediarouter/res/values-de/strings.xml b/v7/mediarouter/res/values-de/strings.xml
index 91d764f..a63feb4 100644
--- a/v7/mediarouter/res/values-de/strings.xml
+++ b/v7/mediarouter/res/values-de/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Streamen auf"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Geräte werden gesucht."</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Verbindung trennen"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Streaming stoppen"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Streaming stoppen"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Schließen"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Wiedergeben"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausieren"</string>
diff --git a/v7/mediarouter/res/values-el/strings.xml b/v7/mediarouter/res/values-el/strings.xml
index 0a1a62f..01a9939 100644
--- a/v7/mediarouter/res/values-el/strings.xml
+++ b/v7/mediarouter/res/values-el/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Μετάδοση σε"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Εύρεση συσκευών"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Αποσύνδεση"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Διακοπή μετάδοσης"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Διακοπή μετάδοσης"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Κλείσιμο"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Αναπαραγωγή"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Παύση"</string>
diff --git a/v7/mediarouter/res/values-en-rAU/strings.xml b/v7/mediarouter/res/values-en-rAU/strings.xml
index d60689e..8b657e1 100644
--- a/v7/mediarouter/res/values-en-rAU/strings.xml
+++ b/v7/mediarouter/res/values-en-rAU/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stop casting"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stop casting"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
diff --git a/v7/mediarouter/res/values-en-rGB/strings.xml b/v7/mediarouter/res/values-en-rGB/strings.xml
index d60689e..8b657e1 100644
--- a/v7/mediarouter/res/values-en-rGB/strings.xml
+++ b/v7/mediarouter/res/values-en-rGB/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stop casting"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stop casting"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
diff --git a/v7/mediarouter/res/values-en-rIN/strings.xml b/v7/mediarouter/res/values-en-rIN/strings.xml
index d60689e..8b657e1 100644
--- a/v7/mediarouter/res/values-en-rIN/strings.xml
+++ b/v7/mediarouter/res/values-en-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stop casting"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stop casting"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
diff --git a/v7/mediarouter/res/values-es-rUS/strings.xml b/v7/mediarouter/res/values-es-rUS/strings.xml
index 2318059..565657c 100644
--- a/v7/mediarouter/res/values-es-rUS/strings.xml
+++ b/v7/mediarouter/res/values-es-rUS/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir a"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Detener la transmisión"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Detener la transmisión"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Cerrar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproducir"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
diff --git a/v7/mediarouter/res/values-es/strings.xml b/v7/mediarouter/res/values-es/strings.xml
index 9f108fe..947c4f19 100644
--- a/v7/mediarouter/res/values-es/strings.xml
+++ b/v7/mediarouter/res/values-es/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Enviar a"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Dejar de enviar contenido"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Dejar de enviar contenido"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Cerrar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproducir"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
diff --git a/v7/mediarouter/res/values-et-rEE/strings.xml b/v7/mediarouter/res/values-et-rEE/strings.xml
index 3fab845..c00e91d 100644
--- a/v7/mediarouter/res/values-et-rEE/strings.xml
+++ b/v7/mediarouter/res/values-et-rEE/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Ülekandmine seadmesse"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Seadmete otsimine"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Katkesta ühendus"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Peata ülekanne"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Peata ülekanne"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Sulgemine"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Esitamine"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Peatamine"</string>
diff --git a/v7/mediarouter/res/values-eu-rES/strings.xml b/v7/mediarouter/res/values-eu-rES/strings.xml
index bae67ed..7f7cb21 100644
--- a/v7/mediarouter/res/values-eu-rES/strings.xml
+++ b/v7/mediarouter/res/values-eu-rES/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Igorri hona"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Gailuak bilatzen"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Deskonektatu"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Utzi igortzeari"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Utzi igortzeari"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Itxi"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Erreproduzitu"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausatu"</string>
diff --git a/v7/mediarouter/res/values-fa/strings.xml b/v7/mediarouter/res/values-fa/strings.xml
index eb34931..1e908c6 100644
--- a/v7/mediarouter/res/values-fa/strings.xml
+++ b/v7/mediarouter/res/values-fa/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ارسال محتوا به"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"پیدا کردن دستگاه‌ها"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"قطع ارتباط"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"توقف ارسال محتوا"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"توقف ارسال محتوا"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"بستن"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"پخش"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"مکث"</string>
diff --git a/v7/mediarouter/res/values-fi/strings.xml b/v7/mediarouter/res/values-fi/strings.xml
index f37317a..b38eaff 100644
--- a/v7/mediarouter/res/values-fi/strings.xml
+++ b/v7/mediarouter/res/values-fi/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Suoratoiston kohde"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Etsitään laitteita"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Katkaise yhteys"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Lopeta suoratoisto"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Lopeta suoratoisto"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Sulje"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Toista"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Keskeytä"</string>
diff --git a/v7/mediarouter/res/values-fr-rCA/strings.xml b/v7/mediarouter/res/values-fr-rCA/strings.xml
index 5719479..6c0334c 100644
--- a/v7/mediarouter/res/values-fr-rCA/strings.xml
+++ b/v7/mediarouter/res/values-fr-rCA/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Diffuser sur"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Recherche d\'appareils"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Se déconnecter"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Arrêter la diffusion"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Arrêter la diffusion"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Fermer"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Lire"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Interrompre"</string>
diff --git a/v7/mediarouter/res/values-fr/strings.xml b/v7/mediarouter/res/values-fr/strings.xml
index 6ce8329..47f2355 100644
--- a/v7/mediarouter/res/values-fr/strings.xml
+++ b/v7/mediarouter/res/values-fr/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Caster sur"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Recherche d\'appareils en cours…"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Déconnecter"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Arrêter de diffuser"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Arrêter de diffuser"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Fermer"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Lecture"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
diff --git a/v7/mediarouter/res/values-gl-rES/strings.xml b/v7/mediarouter/res/values-gl-rES/strings.xml
index c922b68..fa2235e 100644
--- a/v7/mediarouter/res/values-gl-rES/strings.xml
+++ b/v7/mediarouter/res/values-gl-rES/strings.xml
@@ -23,6 +23,7 @@
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
     <string name="mr_controller_stop" msgid="4570331844078181931">"Parar de emitir"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Deter emisión"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Pechar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproduce"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
diff --git a/v7/mediarouter/res/values-gu-rIN/strings.xml b/v7/mediarouter/res/values-gu-rIN/strings.xml
index e3be8fc2..cf027e3 100644
--- a/v7/mediarouter/res/values-gu-rIN/strings.xml
+++ b/v7/mediarouter/res/values-gu-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"આના પર કાસ્ટ કરો"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ઉપકરણો શોધી રહ્યાં છીએ"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ડિસ્કનેક્ટ કરો"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"કાસ્ટ કરવાનું રોકો"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"કાસ્ટ કરવાનું રોકો"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"બંધ કરો"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ચલાવો"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"થોભાવો"</string>
diff --git a/v7/mediarouter/res/values-hi/strings.xml b/v7/mediarouter/res/values-hi/strings.xml
index 9d0650b..712db96 100644
--- a/v7/mediarouter/res/values-hi/strings.xml
+++ b/v7/mediarouter/res/values-hi/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"इस पर कास्‍ट करें"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"डिवाइस ढूंढ रहा है"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"डिस्कनेक्ट करें"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"कास्ट करना बंद करें"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"कास्ट करना बंद करें"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"बंद करें"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"चलाएं"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"रोकें"</string>
diff --git a/v7/mediarouter/res/values-hr/strings.xml b/v7/mediarouter/res/values-hr/strings.xml
index 371088b..f9336c0 100644
--- a/v7/mediarouter/res/values-hr/strings.xml
+++ b/v7/mediarouter/res/values-hr/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Emitiranje na"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Traženje uređaja"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zaustavi emitiranje"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zaustavi emitiranje"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvaranje"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reprodukcija"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziranje"</string>
diff --git a/v7/mediarouter/res/values-hu/strings.xml b/v7/mediarouter/res/values-hu/strings.xml
index a3d6990..3797835 100644
--- a/v7/mediarouter/res/values-hu/strings.xml
+++ b/v7/mediarouter/res/values-hu/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Átküldés ide"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Eszközök keresése"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Leválasztás"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Átküldés leállítása"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Átküldés leállítása"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Bezárás"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Lejátszás"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Szüneteltetés"</string>
diff --git a/v7/mediarouter/res/values-hy-rAM/strings.xml b/v7/mediarouter/res/values-hy-rAM/strings.xml
index a8c1cf3..2e40c65 100644
--- a/v7/mediarouter/res/values-hy-rAM/strings.xml
+++ b/v7/mediarouter/res/values-hy-rAM/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Ընտրեք սարքը"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Սարքերի որոնում"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Անջատել"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Դադարեցնել հեռարձակումը"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Դադարեցնել հեռարձակումը"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Փակել"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Նվագարկել"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Դադար"</string>
diff --git a/v7/mediarouter/res/values-in/strings.xml b/v7/mediarouter/res/values-in/strings.xml
index 4bc0852..2717ec1 100644
--- a/v7/mediarouter/res/values-in/strings.xml
+++ b/v7/mediarouter/res/values-in/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmisikan ke"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Mencari perangkat"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Putuskan sambungan"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Hentikan transmisi"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Hentikan transmisi"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Tutup"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Putar"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Jeda"</string>
diff --git a/v7/mediarouter/res/values-is-rIS/strings.xml b/v7/mediarouter/res/values-is-rIS/strings.xml
index 08e41e6..58d3e0c 100644
--- a/v7/mediarouter/res/values-is-rIS/strings.xml
+++ b/v7/mediarouter/res/values-is-rIS/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Senda út í"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Leitað að tækjum"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Aftengjast"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stöðva útsendingu"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stöðva útsendingu"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Loka"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Spila"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Hlé"</string>
diff --git a/v7/mediarouter/res/values-it/strings.xml b/v7/mediarouter/res/values-it/strings.xml
index 87b570c..927c813 100644
--- a/v7/mediarouter/res/values-it/strings.xml
+++ b/v7/mediarouter/res/values-it/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Trasmetti a"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Ricerca di dispositivi in corso"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Scollega"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Interrompi trasmissione"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Interrompi trasmissione"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Chiudi"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Riproduci"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
diff --git a/v7/mediarouter/res/values-iw/strings.xml b/v7/mediarouter/res/values-iw/strings.xml
index 8b52adf..1ae3085 100644
--- a/v7/mediarouter/res/values-iw/strings.xml
+++ b/v7/mediarouter/res/values-iw/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"העבר אל"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"מחפש מכשירים"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"נתק"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"עצור העברה"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"עצור העברה"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"סגור"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"הפעל"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"השהה"</string>
diff --git a/v7/mediarouter/res/values-ja/strings.xml b/v7/mediarouter/res/values-ja/strings.xml
index b126965..60dbbff 100644
--- a/v7/mediarouter/res/values-ja/strings.xml
+++ b/v7/mediarouter/res/values-ja/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"キャストするデバイス"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"端末を検索しています"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"接続を解除"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"キャストを停止"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"キャストを停止"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"閉じる"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"再生"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"一時停止"</string>
diff --git a/v7/mediarouter/res/values-ka-rGE/strings.xml b/v7/mediarouter/res/values-ka-rGE/strings.xml
index 046e361..3a5ced7 100644
--- a/v7/mediarouter/res/values-ka-rGE/strings.xml
+++ b/v7/mediarouter/res/values-ka-rGE/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ტრანსლირებული"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"მიმდინარეობს მოწყობილობების მოძიება"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"კავშირის გაწყვეტა"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ტრანსლირების შეჩერება"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ტრანსლირების შეჩერება"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"დახურვა"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"დაკვრა"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"პაუზა"</string>
diff --git a/v7/mediarouter/res/values-kk-rKZ/strings.xml b/v7/mediarouter/res/values-kk-rKZ/strings.xml
index 5cf4e5a2..e6912bc 100644
--- a/v7/mediarouter/res/values-kk-rKZ/strings.xml
+++ b/v7/mediarouter/res/values-kk-rKZ/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Келесіге трансляциялау"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Құрылғыларды табу"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ажырату"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Трансляциялауды тоқтату"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Трансляциялауды тоқтату"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Жабу"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Ойнату"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Кідірту"</string>
diff --git a/v7/mediarouter/res/values-km-rKH/strings.xml b/v7/mediarouter/res/values-km-rKH/strings.xml
index fd05668..b669d94 100644
--- a/v7/mediarouter/res/values-km-rKH/strings.xml
+++ b/v7/mediarouter/res/values-km-rKH/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ខាសទៅ"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ស្វែងរកឧបករណ៍"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ផ្ដាច់"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"បញ្ឈប់ការខាស"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"បញ្ឈប់ការខាស"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"បិទ"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ចាក់"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ផ្អាក"</string>
diff --git a/v7/mediarouter/res/values-kn-rIN/strings.xml b/v7/mediarouter/res/values-kn-rIN/strings.xml
index 9cae5be..688153c 100644
--- a/v7/mediarouter/res/values-kn-rIN/strings.xml
+++ b/v7/mediarouter/res/values-kn-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ಇದಕ್ಕೆ ಬಿತ್ತರಿಸಿ"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ಸಾಧನಗಳನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ಸಂಪರ್ಕ ಕಡಿತಗೊಳಿಸು"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ಬಿತ್ತರಿಸುವಿಕೆ ನಿಲ್ಲಿಸು"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ಬಿತ್ತರಿಸುವಿಕೆ ನಿಲ್ಲಿಸು"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ಮುಚ್ಚು"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ಪ್ಲೇ ಮಾಡಿ"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ವಿರಾಮ"</string>
diff --git a/v7/mediarouter/res/values-ko/strings.xml b/v7/mediarouter/res/values-ko/strings.xml
index 7f53382..216a9ea 100644
--- a/v7/mediarouter/res/values-ko/strings.xml
+++ b/v7/mediarouter/res/values-ko/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"전송할 기기"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"기기를 찾는 중"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"연결 해제"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"전송 중지"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"전송 중지"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"닫기"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"재생"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"일시중지"</string>
diff --git a/v7/mediarouter/res/values-ky-rKG/strings.xml b/v7/mediarouter/res/values-ky-rKG/strings.xml
index 99201dc..bd37edc 100644
--- a/v7/mediarouter/res/values-ky-rKG/strings.xml
+++ b/v7/mediarouter/res/values-ky-rKG/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Төмөнкүгө чыгаруу"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Түзмөктөр изделүүдө"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ажыратуу"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Тышк экранга чыгарну токтотуу"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Тышк экранга чыгарну токтотуу"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Жабуу"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Ойнотуу"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Тындыруу"</string>
diff --git a/v7/mediarouter/res/values-lo-rLA/strings.xml b/v7/mediarouter/res/values-lo-rLA/strings.xml
index 2765364..fac1c33 100644
--- a/v7/mediarouter/res/values-lo-rLA/strings.xml
+++ b/v7/mediarouter/res/values-lo-rLA/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ຄາ​ສ​ທ໌​ຫາ"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ກຳລັງ​ຊອກ​ຫາ​ອຸ​ປະ​ກອນ"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ຕັດການເຊື່ອມຕໍ່"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ຢຸດການຄາ​ສ​ທ໌"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ຢຸດການຄາ​ສ​ທ໌"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ປິດ"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ຫຼິ້ນ"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ຢຸດຊົ່ວຄາວ"</string>
diff --git a/v7/mediarouter/res/values-lt/strings.xml b/v7/mediarouter/res/values-lt/strings.xml
index 208752f..5cd8c69 100644
--- a/v7/mediarouter/res/values-lt/strings.xml
+++ b/v7/mediarouter/res/values-lt/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Perduoti į"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Randami įrenginiai"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Atjungti"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Sustabdyti perdavimą"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Sustabdyti perdavimą"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Uždaryti"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Leisti"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pristabdyti"</string>
diff --git a/v7/mediarouter/res/values-lv/strings.xml b/v7/mediarouter/res/values-lv/strings.xml
index 832a3ba..034063b 100644
--- a/v7/mediarouter/res/values-lv/strings.xml
+++ b/v7/mediarouter/res/values-lv/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Apraidīšana uz ierīci"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Notiek ierīču meklēšana"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Atvienot"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Pārtraukt apraidi"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Pārtraukt apraidi"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Aizvērt"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Atskaņot"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Apturēt"</string>
diff --git a/v7/mediarouter/res/values-mk-rMK/strings.xml b/v7/mediarouter/res/values-mk-rMK/strings.xml
index 726e285..a2aceea 100644
--- a/v7/mediarouter/res/values-mk-rMK/strings.xml
+++ b/v7/mediarouter/res/values-mk-rMK/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Емитувај на"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Наоѓање уреди"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Исклучи"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Запри го емитувањето"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Запри го емитувањето"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Затвори"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Репродуцирај"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Паузирај"</string>
diff --git a/v7/mediarouter/res/values-ml-rIN/strings.xml b/v7/mediarouter/res/values-ml-rIN/strings.xml
index b1d2cbe..3df50d9 100644
--- a/v7/mediarouter/res/values-ml-rIN/strings.xml
+++ b/v7/mediarouter/res/values-ml-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ഇതിലേക്ക് കാസ്റ്റുചെയ്യുക"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ഉപകരണങ്ങൾ കണ്ടെത്തുന്നു"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"വിച്ഛേദിക്കുക"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"കാസ്റ്റുചെയ്യൽ നിർത്തുക"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"കാസ്റ്റുചെയ്യൽ നിർത്തുക"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"അടയ്‌ക്കുക"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"പ്ലേ ചെയ്യുക"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"തൽക്കാലം നിർത്തൂ"</string>
diff --git a/v7/mediarouter/res/values-mn-rMN/strings.xml b/v7/mediarouter/res/values-mn-rMN/strings.xml
index d07d314..18792a8 100644
--- a/v7/mediarouter/res/values-mn-rMN/strings.xml
+++ b/v7/mediarouter/res/values-mn-rMN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Дамжуулах"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Төхөөрөмж хайж байна"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Салгах"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Дамжуулахыг зогсоох"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Дамжуулахыг зогсоох"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Хаах"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Тоглуулах"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Түр зогсоох"</string>
diff --git a/v7/mediarouter/res/values-mr-rIN/strings.xml b/v7/mediarouter/res/values-mr-rIN/strings.xml
index 4e24aff..ba8a835 100644
--- a/v7/mediarouter/res/values-mr-rIN/strings.xml
+++ b/v7/mediarouter/res/values-mr-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"यावर कास्ट करा"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"डिव्हाइसेस शोधत आहे"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"डिस्‍कनेक्‍ट करा"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"कास्ट करणे थांबवा"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"कास्ट करणे थांबवा"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"बंद करा"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"प्ले करा"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"विराम"</string>
diff --git a/v7/mediarouter/res/values-ms-rMY/strings.xml b/v7/mediarouter/res/values-ms-rMY/strings.xml
index 2a4cd1c..ceb7ca1 100644
--- a/v7/mediarouter/res/values-ms-rMY/strings.xml
+++ b/v7/mediarouter/res/values-ms-rMY/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Hantar ke"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Mencari peranti"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Putuskan sambungan"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Berhenti menghantar"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Berhenti menghantar"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Tutup"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Main"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Jeda"</string>
diff --git a/v7/mediarouter/res/values-my-rMM/strings.xml b/v7/mediarouter/res/values-my-rMM/strings.xml
index eca8835..66f43b9 100644
--- a/v7/mediarouter/res/values-my-rMM/strings.xml
+++ b/v7/mediarouter/res/values-my-rMM/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"သို့ ကာစ်တ်လုပ်ရန်"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"စက်ပစ္စည်းများ ရှာဖွေခြင်း"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ဆက်သွယ်မှု ဖြတ်ရန်"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ကာစ်တ်လုပ်ခြင်း ရပ်တန့်ရန်"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ကာစ်တ်လုပ်ခြင်း ရပ်တန့်ရန်"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ပိတ်ရန်"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ဖွင့်ရန်"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ခဏရပ်ရန်"</string>
diff --git a/v7/mediarouter/res/values-nb/strings.xml b/v7/mediarouter/res/values-nb/strings.xml
index 27f9f03..2cb16d8 100644
--- a/v7/mediarouter/res/values-nb/strings.xml
+++ b/v7/mediarouter/res/values-nb/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Cast til"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Finner enheter"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Koble fra"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Stopp castingen"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Stopp castingen"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Lukk"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Spill av"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Sett på pause"</string>
diff --git a/v7/mediarouter/res/values-ne-rNP/strings.xml b/v7/mediarouter/res/values-ne-rNP/strings.xml
index 6abadbf..380b52f 100644
--- a/v7/mediarouter/res/values-ne-rNP/strings.xml
+++ b/v7/mediarouter/res/values-ne-rNP/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"यसमा Cast गर्नुहोस्"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"यन्त्रहरू पत्ता लगाउँदै"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"विच्छेद गर्नुहोस्"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"casting रोक्नुहोस्"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"casting रोक्नुहोस्"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"बन्द गर्नुहोस्"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"बजाउनुहोस्"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"रोक्नुहोस्"</string>
diff --git a/v7/mediarouter/res/values-nl/strings.xml b/v7/mediarouter/res/values-nl/strings.xml
index 4a9346d..d204db8 100644
--- a/v7/mediarouter/res/values-nl/strings.xml
+++ b/v7/mediarouter/res/values-nl/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Casten naar"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Apparaten zoeken"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Loskoppelen"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Casten stoppen"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Casten stoppen"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Sluiten"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Afspelen"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Onderbreken"</string>
diff --git a/v7/mediarouter/res/values-pa-rIN/strings.xml b/v7/mediarouter/res/values-pa-rIN/strings.xml
index 842a8b4..ed420ba 100644
--- a/v7/mediarouter/res/values-pa-rIN/strings.xml
+++ b/v7/mediarouter/res/values-pa-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"ਇਸ ਨਾਲ ਕਾਸਟ ਕਰੋ"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"ਡਿਵਾਈਸਾਂ ਲੱਭ ਰਿਹਾ ਹੈ"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ਡਿਸਕਨੈਕਟ ਕਰੋ"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ਜੋੜਨਾ ਰੋਕੋ"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ਜੋੜਨਾ ਰੋਕੋ"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ਬੰਦ ਕਰੋ"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ਪਲੇ ਕਰੋ"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"ਰੋਕੋ"</string>
diff --git a/v7/mediarouter/res/values-pl/strings.xml b/v7/mediarouter/res/values-pl/strings.xml
index d66be1a..4461f56 100644
--- a/v7/mediarouter/res/values-pl/strings.xml
+++ b/v7/mediarouter/res/values-pl/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Przesyłaj na"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Znajdowanie urządzeń"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odłącz"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zatrzymaj przesyłanie"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zatrzymaj przesyłanie"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zamknij"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Odtwórz"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Wstrzymaj"</string>
diff --git a/v7/mediarouter/res/values-pt-rBR/strings.xml b/v7/mediarouter/res/values-pt-rBR/strings.xml
index 4d7e6cc..0b633b9 100644
--- a/v7/mediarouter/res/values-pt-rBR/strings.xml
+++ b/v7/mediarouter/res/values-pt-rBR/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Localizando dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Interromper transmissão"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Interromper transmissão"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
diff --git a/v7/mediarouter/res/values-pt-rPT/strings.xml b/v7/mediarouter/res/values-pt-rPT/strings.xml
index 0c68b92..c96c23a 100644
--- a/v7/mediarouter/res/values-pt-rPT/strings.xml
+++ b/v7/mediarouter/res/values-pt-rPT/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"A localizar dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desassociar"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Interromper a transmissão"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Interromper a transmissão"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Interromper"</string>
diff --git a/v7/mediarouter/res/values-pt/strings.xml b/v7/mediarouter/res/values-pt/strings.xml
index 4d7e6cc..0b633b9 100644
--- a/v7/mediarouter/res/values-pt/strings.xml
+++ b/v7/mediarouter/res/values-pt/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Localizando dispositivos"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Interromper transmissão"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Interromper transmissão"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
diff --git a/v7/mediarouter/res/values-ro/strings.xml b/v7/mediarouter/res/values-ro/strings.xml
index 9fe26a9..3c3853c 100644
--- a/v7/mediarouter/res/values-ro/strings.xml
+++ b/v7/mediarouter/res/values-ro/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Proiectați pe"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Se caută dispozitive"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Deconectați-vă"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Opriți proiecția"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Opriți proiecția"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Închideți"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Redați"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Întrerupeți"</string>
diff --git a/v7/mediarouter/res/values-ru/strings.xml b/v7/mediarouter/res/values-ru/strings.xml
index 4607a8c..1927dc6 100644
--- a/v7/mediarouter/res/values-ru/strings.xml
+++ b/v7/mediarouter/res/values-ru/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Выберите устройство"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Поиск устройств…"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Отключить"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Остановить трансляцию"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Остановить трансляцию"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрыть"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Воспроизвести"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Приостановить"</string>
diff --git a/v7/mediarouter/res/values-si-rLK/strings.xml b/v7/mediarouter/res/values-si-rLK/strings.xml
index 144a0d5..f4d1869 100644
--- a/v7/mediarouter/res/values-si-rLK/strings.xml
+++ b/v7/mediarouter/res/values-si-rLK/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"විකාශය"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"උපාංග සෙවීම"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"විසන්ධි කරන්න"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"විකාශ කිරීම නවත්වන්න"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"විකාශ කිරීම නවත්වන්න"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"වසන්න"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ධාවනය කරන්න"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"විරාම ගන්වන්න"</string>
diff --git a/v7/mediarouter/res/values-sk/strings.xml b/v7/mediarouter/res/values-sk/strings.xml
index b546bde..47d9aaf 100644
--- a/v7/mediarouter/res/values-sk/strings.xml
+++ b/v7/mediarouter/res/values-sk/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Prenos do"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Vyhľadávanie zariadení"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odpojiť"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Zastaviť prenos"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Zastaviť prenos"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zavrieť"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Prehrať"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pozastaviť"</string>
diff --git a/v7/mediarouter/res/values-sl/strings.xml b/v7/mediarouter/res/values-sl/strings.xml
index 110c548..39f5935 100644
--- a/v7/mediarouter/res/values-sl/strings.xml
+++ b/v7/mediarouter/res/values-sl/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Predvajanje prek:"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Iskanje naprav"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini povezavo"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Ustavi predvajanje"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Ustavi predvajanje"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Zapri"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Predvajanje"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Zaustavi"</string>
diff --git a/v7/mediarouter/res/values-sq-rAL/strings.xml b/v7/mediarouter/res/values-sq-rAL/strings.xml
index 8ed93c3..77e1b5d 100644
--- a/v7/mediarouter/res/values-sq-rAL/strings.xml
+++ b/v7/mediarouter/res/values-sq-rAL/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Transmeto te"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Gjetja e pajisjeve"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Shkëpute"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Ndalo transmetimin"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Ndalo transmetimin"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Mbyll"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Luaj"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pauzë"</string>
diff --git a/v7/mediarouter/res/values-sr/strings.xml b/v7/mediarouter/res/values-sr/strings.xml
index 5a72bd4..0343c71 100644
--- a/v7/mediarouter/res/values-sr/strings.xml
+++ b/v7/mediarouter/res/values-sr/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Пребацујте на"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Проналажење уређаја"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Прекини везу"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Заустави пребацивање"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Заустави пребацивање"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Затвори"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Пусти"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Паузирај"</string>
diff --git a/v7/mediarouter/res/values-sv/strings.xml b/v7/mediarouter/res/values-sv/strings.xml
index 3724902..e24d371 100644
--- a/v7/mediarouter/res/values-sv/strings.xml
+++ b/v7/mediarouter/res/values-sv/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Casta till"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Letar efter enheter"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Koppla från"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Sluta casta"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Sluta casta"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Stäng"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Spela upp"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
diff --git a/v7/mediarouter/res/values-sw/strings.xml b/v7/mediarouter/res/values-sw/strings.xml
index f12fd5c..e48b683 100644
--- a/v7/mediarouter/res/values-sw/strings.xml
+++ b/v7/mediarouter/res/values-sw/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Tuma kwenye"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Inatafuta vifaa"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ondoa"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Acha kutuma"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Acha kutuma"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Funga"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Cheza"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Sitisha"</string>
diff --git a/v7/mediarouter/res/values-ta-rIN/strings.xml b/v7/mediarouter/res/values-ta-rIN/strings.xml
index c314178..a2badf4 100644
--- a/v7/mediarouter/res/values-ta-rIN/strings.xml
+++ b/v7/mediarouter/res/values-ta-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"இதில் திரையிடு"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"சாதனங்களைத் தேடுகிறது"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"தொடர்பைத் துண்டி"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"அனுப்புவதை நிறுத்து"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"அனுப்புவதை நிறுத்து"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"மூடும்"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"இயக்கும்"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"இடைநிறுத்தும்"</string>
diff --git a/v7/mediarouter/res/values-te-rIN/strings.xml b/v7/mediarouter/res/values-te-rIN/strings.xml
index 59a4f19..1bed787 100644
--- a/v7/mediarouter/res/values-te-rIN/strings.xml
+++ b/v7/mediarouter/res/values-te-rIN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"దీనికి ప్రసారం చేయండి"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"పరికరాలను కనుగొంటోంది"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"డిస్‌కనెక్ట్ చేయి"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"ప్రసారాన్ని ఆపివేయి"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"ప్రసారాన్ని ఆపివేయి"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"మూసివేస్తుంది"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"ప్లే చేస్తుంది"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"పాజ్ చేస్తుంది"</string>
diff --git a/v7/mediarouter/res/values-th/strings.xml b/v7/mediarouter/res/values-th/strings.xml
index 1bfd091..37fcad8 100644
--- a/v7/mediarouter/res/values-th/strings.xml
+++ b/v7/mediarouter/res/values-th/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"แคสต์ไปยัง"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"กำลังค้นหาอุปกรณ์"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"ยกเลิกการเชื่อมต่อ"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"หยุดการแคสต์"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"หยุดการแคสต์"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"ปิด"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"เล่น"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"หยุดชั่วคราว"</string>
diff --git a/v7/mediarouter/res/values-tl/strings.xml b/v7/mediarouter/res/values-tl/strings.xml
index 82396e1..8e2d9d1 100644
--- a/v7/mediarouter/res/values-tl/strings.xml
+++ b/v7/mediarouter/res/values-tl/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"I-cast sa"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Naghahanap ng mga device"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Idiskonekta"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Itigil ang pagca-cast"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Itigil ang pagca-cast"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Isara"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"I-play"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"I-pause"</string>
diff --git a/v7/mediarouter/res/values-tr/strings.xml b/v7/mediarouter/res/values-tr/strings.xml
index e639963..089d2ab 100644
--- a/v7/mediarouter/res/values-tr/strings.xml
+++ b/v7/mediarouter/res/values-tr/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Şuraya yayınla:"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Cihazlar bulunuyor"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Bağlantıyı kes"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Yayını durdur"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Yayını durdur"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Kapat"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Oynat"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Duraklat"</string>
diff --git a/v7/mediarouter/res/values-uk/strings.xml b/v7/mediarouter/res/values-uk/strings.xml
index a768e2c..33dd659 100644
--- a/v7/mediarouter/res/values-uk/strings.xml
+++ b/v7/mediarouter/res/values-uk/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Транслювати на"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Пошук пристроїв"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Відключити"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Припинити трансляцію"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Припинити трансляцію"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрити"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Відтворити"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Призупинити"</string>
diff --git a/v7/mediarouter/res/values-ur-rPK/strings.xml b/v7/mediarouter/res/values-ur-rPK/strings.xml
index 5cb3b36..51b4a46 100644
--- a/v7/mediarouter/res/values-ur-rPK/strings.xml
+++ b/v7/mediarouter/res/values-ur-rPK/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"اس میں کاسٹ کریں"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"آلات تلاش ہو رہے ہیں"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"غیر منسلک کریں"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"کاسٹ کرنا بند کریں"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"کاسٹ کرنا بند کریں"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"بند کریں"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"چلائیں"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"موقوف کریں"</string>
diff --git a/v7/mediarouter/res/values-uz-rUZ/strings.xml b/v7/mediarouter/res/values-uz-rUZ/strings.xml
index 9955cdfd..b80bb67 100644
--- a/v7/mediarouter/res/values-uz-rUZ/strings.xml
+++ b/v7/mediarouter/res/values-uz-rUZ/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Quyidagiga translatsiya qilish:"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Qurilmalarni topish"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ulanishni uzish"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Translatsiyani to‘xtatish"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Translatsiyani to‘xtatish"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Yopish"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Boshlash"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"To‘xtatib turish"</string>
diff --git a/v7/mediarouter/res/values-vi/strings.xml b/v7/mediarouter/res/values-vi/strings.xml
index 0080e3e..9229ba2 100644
--- a/v7/mediarouter/res/values-vi/strings.xml
+++ b/v7/mediarouter/res/values-vi/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Truyền tới"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Tìm thiết bị"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ngắt kết nối"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Dừng truyền"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Dừng truyền"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Đóng"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Phát"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Tạm dừng"</string>
diff --git a/v7/mediarouter/res/values-zh-rCN/strings.xml b/v7/mediarouter/res/values-zh-rCN/strings.xml
index aabe727..22fa4ce 100644
--- a/v7/mediarouter/res/values-zh-rCN/strings.xml
+++ b/v7/mediarouter/res/values-zh-rCN/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"投射到"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"正在查找设备"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"断开连接"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"停止投射"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"停止投射"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"关闭"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"暂停"</string>
diff --git a/v7/mediarouter/res/values-zh-rHK/strings.xml b/v7/mediarouter/res/values-zh-rHK/strings.xml
index d01c823..afabbde 100644
--- a/v7/mediarouter/res/values-zh-rHK/strings.xml
+++ b/v7/mediarouter/res/values-zh-rHK/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"投放至"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"正在尋找裝置"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"中斷連線"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"停止投放"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"停止投放"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"關閉"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"暫停"</string>
diff --git a/v7/mediarouter/res/values-zh-rTW/strings.xml b/v7/mediarouter/res/values-zh-rTW/strings.xml
index 68347e5..1a62c35 100644
--- a/v7/mediarouter/res/values-zh-rTW/strings.xml
+++ b/v7/mediarouter/res/values-zh-rTW/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"投放到"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"正在尋找裝置"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"中斷連線"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"停止投放"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"停止投放"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"關閉"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"暫停"</string>
diff --git a/v7/mediarouter/res/values-zu/strings.xml b/v7/mediarouter/res/values-zu/strings.xml
index e50acc8..4e70355 100644
--- a/v7/mediarouter/res/values-zu/strings.xml
+++ b/v7/mediarouter/res/values-zu/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_chooser_title" msgid="414301941546135990">"Sakaza ku-"</string>
     <string name="mr_chooser_searching" msgid="6349900579507521956">"Ithola amadivayisi"</string>
     <string name="mr_controller_disconnect" msgid="1227264889412989580">"Nqamula"</string>
-    <string name="mr_controller_stop" msgid="4570331844078181931">"Misa ukusakaza"</string>
+    <string name="mr_controller_stop_casting" msgid="4570331844078181931">"Misa ukusakaza"</string>
     <string name="mr_controller_close_description" msgid="7333862312480583260">"Vala"</string>
     <string name="mr_controller_play" msgid="683634565969987458">"Dlala"</string>
     <string name="mr_controller_pause" msgid="5451884435510905406">"Misa isikhashana"</string>
diff --git a/v7/mediarouter/res/values/attrs.xml b/v7/mediarouter/res/values/attrs.xml
index c618fd8..10d1c2c8 100644
--- a/v7/mediarouter/res/values/attrs.xml
+++ b/v7/mediarouter/res/values/attrs.xml
@@ -32,6 +32,7 @@
     <attr name="mediaRouteCloseDrawable" format="reference" />
     <attr name="mediaRoutePlayDrawable" format="reference" />
     <attr name="mediaRoutePauseDrawable" format="reference" />
+    <attr name="mediaRouteStopDrawable" format="reference" />
     <attr name="mediaRouteAudioTrackDrawable" format="reference" />
     <attr name="mediaRouteDefaultIconDrawable" format="reference" />
     <attr name="mediaRouteTvIconDrawable" format="reference" />
diff --git a/v7/mediarouter/res/values/strings.xml b/v7/mediarouter/res/values/strings.xml
index bff7176..630a482 100644
--- a/v7/mediarouter/res/values/strings.xml
+++ b/v7/mediarouter/res/values/strings.xml
@@ -47,7 +47,7 @@
     <string name="mr_controller_disconnect">Disconnect</string>
 
     <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=30] -->
-    <string name="mr_controller_stop">Stop casting</string>
+    <string name="mr_controller_stop_casting">Stop casting</string>
 
     <!-- Content description for accessibility (not shown on the screen): dialog close button. [CHAR LIMIT=NONE] -->
     <string name="mr_controller_close_description">Close</string>
@@ -58,6 +58,9 @@
     <!-- Content description for accessibility (not shown on the screen): media pause button. [CHAR LIMIT=NONE] -->
     <string name="mr_controller_pause">Pause</string>
 
+    <!-- Content description for accessibility (not shown on the screen): media stop button. [CHAR LIMIT=NONE] -->
+    <string name="mr_controller_stop">Stop</string>
+
     <!-- Content description for accessibility (not shown on the screen): group expand button. Pressing button shows group members of a selected route group. [CHAR LIMIT=NONE] -->
     <string name="mr_controller_expand_group">Expand</string>
 
diff --git a/v7/mediarouter/res/values/themes.xml b/v7/mediarouter/res/values/themes.xml
index 1eb4bfd..8c6e97a 100644
--- a/v7/mediarouter/res/values/themes.xml
+++ b/v7/mediarouter/res/values/themes.xml
@@ -23,6 +23,7 @@
         <item name="mediaRouteCloseDrawable">@drawable/mr_dialog_close_dark</item>
         <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_dark</item>
         <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_dark</item>
+        <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_dark</item>
         <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_dark</item>
         <item name="mediaRouteDefaultIconDrawable">@drawable/ic_mr_button_disconnected_dark</item>
         <item name="mediaRouteTvIconDrawable">@drawable/ic_vol_type_tv_dark</item>
@@ -43,6 +44,7 @@
         <item name="mediaRouteCloseDrawable">@drawable/mr_dialog_close_light</item>
         <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_light</item>
         <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_light</item>
+        <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_light</item>
         <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_light</item>
         <item name="mediaRouteDefaultIconDrawable">@drawable/ic_mr_button_grey</item>
         <item name="mediaRouteTvIconDrawable">@drawable/ic_vol_type_tv_light</item>
@@ -59,12 +61,14 @@
     <style name="ThemeOverlay.MediaRouter.Dark" parent="ThemeOverlay.AppCompat.Dark">
         <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_dark</item>
         <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_dark</item>
+        <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_dark</item>
         <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_dark</item>
 
     </style>
     <style name="ThemeOverlay.MediaRouter.Light" parent="ThemeOverlay.AppCompat.Light">
         <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_light</item>
         <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_light</item>
+        <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_light</item>
         <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_light</item>
     </style>
 
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
index 961e37e..4d40610 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -16,6 +16,11 @@
 
 package android.support.v7.app;
 
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP;
+
 import android.app.PendingIntent;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -67,7 +72,6 @@
 import android.widget.RelativeLayout;
 import android.widget.SeekBar;
 import android.widget.TextView;
-
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -119,7 +123,7 @@
 
     private Button mDisconnectButton;
     private Button mStopCastingButton;
-    private ImageButton mPlayPauseButton;
+    private ImageButton mPlaybackControlButton;
     private ImageButton mCloseButton;
     private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;
 
@@ -351,7 +355,7 @@
         mDisconnectButton.setOnClickListener(listener);
 
         mStopCastingButton = (Button) findViewById(BUTTON_STOP_RES_ID);
-        mStopCastingButton.setText(R.string.mr_controller_stop);
+        mStopCastingButton.setText(R.string.mr_controller_stop_casting);
         mStopCastingButton.setTextColor(color);
         mStopCastingButton.setOnClickListener(listener);
 
@@ -388,8 +392,8 @@
         mPlaybackControlLayout = (RelativeLayout) findViewById(R.id.mr_playback_control);
         mTitleView = (TextView) findViewById(R.id.mr_control_title);
         mSubtitleView = (TextView) findViewById(R.id.mr_control_subtitle);
-        mPlayPauseButton = (ImageButton) findViewById(R.id.mr_control_play_pause);
-        mPlayPauseButton.setOnClickListener(listener);
+        mPlaybackControlButton = (ImageButton) findViewById(R.id.mr_control_playback_ctrl);
+        mPlaybackControlButton.setOnClickListener(listener);
 
         mVolumeControlLayout = (LinearLayout) findViewById(R.id.mr_volume_control);
         mVolumeControlLayout.setVisibility(View.GONE);
@@ -1006,30 +1010,47 @@
             if (mState != null) {
                 boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING
                         || mState.getState() == PlaybackStateCompat.STATE_PLAYING;
-                boolean supportsPlay = (mState.getActions() & (PlaybackStateCompat.ACTION_PLAY
-                        | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
-                boolean supportsPause = (mState.getActions() & (PlaybackStateCompat.ACTION_PAUSE
-                        | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
-                Context playPauseButtonContext = mPlayPauseButton.getContext();
-                if (isPlaying && supportsPause) {
-                    mPlayPauseButton.setVisibility(View.VISIBLE);
-                    mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource(
-                            playPauseButtonContext, R.attr.mediaRoutePauseDrawable));
-                    mPlayPauseButton.setContentDescription(playPauseButtonContext.getResources()
-                            .getText(R.string.mr_controller_pause));
-                } else if (!isPlaying && supportsPlay) {
-                    mPlayPauseButton.setVisibility(View.VISIBLE);
-                    mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource(
-                            playPauseButtonContext, R.attr.mediaRoutePlayDrawable));
-                    mPlayPauseButton.setContentDescription(playPauseButtonContext.getResources()
-                            .getText(R.string.mr_controller_play));
+                Context playbackControlButtonContext = mPlaybackControlButton.getContext();
+                boolean visible = true;
+                int iconDrawableAttr = 0;
+                int iconDescResId = 0;
+                if (isPlaying && isPauseActionSupported()) {
+                    iconDrawableAttr = R.attr.mediaRoutePauseDrawable;
+                    iconDescResId = R.string.mr_controller_pause;
+                } else if (isPlaying && isStopActionSupported()) {
+                    iconDrawableAttr = R.attr.mediaRouteStopDrawable;
+                    iconDescResId = R.string.mr_controller_stop;
+                } else if (!isPlaying && isPlayActionSupported()) {
+                    iconDrawableAttr = R.attr.mediaRoutePlayDrawable;
+                    iconDescResId = R.string.mr_controller_play;
                 } else {
-                    mPlayPauseButton.setVisibility(View.GONE);
+                    visible = false;
+                }
+                mPlaybackControlButton.setVisibility(visible ? View.VISIBLE : View.GONE);
+                if (visible) {
+                    mPlaybackControlButton.setImageResource(
+                            MediaRouterThemeHelper.getThemeResource(
+                                    playbackControlButtonContext, iconDrawableAttr));
+                    mPlaybackControlButton.setContentDescription(
+                            playbackControlButtonContext.getResources()
+                                    .getText(iconDescResId));
                 }
             }
         }
     }
 
+    private boolean isPlayActionSupported() {
+        return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0;
+    }
+
+    private boolean isPauseActionSupported() {
+        return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0;
+    }
+
+    private boolean isStopActionSupported() {
+        return (mState.getActions() & ACTION_STOP) != 0;
+    }
+
     boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) {
         return mVolumeControlEnabled && route.getVolumeHandling()
                 == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
@@ -1172,23 +1193,28 @@
                             MediaRouter.UNSELECT_REASON_DISCONNECTED);
                 }
                 dismiss();
-            } else if (id == R.id.mr_control_play_pause) {
+            } else if (id == R.id.mr_control_playback_ctrl) {
                 if (mMediaController != null && mState != null) {
                     boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING;
-                    if (isPlaying) {
+                    int actionDescResId = 0;
+                    if (isPlaying && isPauseActionSupported()) {
                         mMediaController.getTransportControls().pause();
-                    } else {
+                        actionDescResId = R.string.mr_controller_pause;
+                    } else if (isPlaying && isStopActionSupported()) {
+                        mMediaController.getTransportControls().stop();
+                        actionDescResId = R.string.mr_controller_stop;
+                    } else if (!isPlaying && isPlayActionSupported()){
                         mMediaController.getTransportControls().play();
+                        actionDescResId = R.string.mr_controller_play;
                     }
                     // Announce the action for accessibility.
-                    if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()) {
+                    if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()
+                            && actionDescResId != 0) {
                         AccessibilityEvent event = AccessibilityEvent.obtain(
                                 AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
                         event.setPackageName(mContext.getPackageName());
                         event.setClassName(getClass().getName());
-                        int resId = isPlaying ?
-                                R.string.mr_controller_pause : R.string.mr_controller_play;
-                        event.getText().add(mContext.getString(resId));
+                        event.getText().add(mContext.getString(actionDescResId));
                         mAccessibilityManager.sendAccessibilityEvent(event);
                     }
                 }
diff --git a/v7/palette/Android.mk b/v7/palette/Android.mk
index c21dad3..a9f9a75 100644
--- a/v7/palette/Android.mk
+++ b/v7/palette/Android.mk
@@ -29,7 +29,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under, src/main)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/src/main/res
-LOCAL_MANIFEST_FILE := src/main/AndroidManifest.xml
+LOCAL_MANIFEST_FILE := src/main/AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-core-utils \
diff --git a/v7/palette/build.gradle b/v7/palette/build.gradle
index 24943703..686fe71 100644
--- a/v7/palette/build.gradle
+++ b/v7/palette/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'palette-v7'
 
 dependencies {
@@ -34,28 +33,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/palette/src/main/AndroidManifest-make.xml b/v7/palette/src/main/AndroidManifest-make.xml
new file mode 100644
index 0000000..5124bc5
--- /dev/null
+++ b/v7/palette/src/main/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v7.palette">
+    <uses-sdk android:minSdkVersion="9"/>
+    <application />
+</manifest>
diff --git a/v7/palette/src/main/AndroidManifest.xml b/v7/palette/src/main/AndroidManifest.xml
index c44818a..52e90a2 100644
--- a/v7/palette/src/main/AndroidManifest.xml
+++ b/v7/palette/src/main/AndroidManifest.xml
@@ -16,6 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v7.palette">
     <uses-sdk android:minSdkVersion="9"/>
-    <application>
-    </application>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
+    <application />
 </manifest>
diff --git a/v7/preference/Android.mk b/v7/preference/Android.mk
index e751e1c..110aed2 100644
--- a/v7/preference/Android.mk
+++ b/v7/preference/Android.mk
@@ -32,6 +32,7 @@
     $(call all-java-files-under,constants) \
     $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-v7-appcompat \
     android-support-v7-recyclerview \
diff --git a/v7/preference/AndroidManifest-make.xml b/v7/preference/AndroidManifest-make.xml
new file mode 100644
index 0000000..9231775
--- /dev/null
+++ b/v7/preference/AndroidManifest-make.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.support.v7.preference">
+    <uses-sdk android:minSdkVersion="9" />
+    <application />
+</manifest>
diff --git a/v7/preference/AndroidManifest.xml b/v7/preference/AndroidManifest.xml
index da9c80e..19e6215 100644
--- a/v7/preference/AndroidManifest.xml
+++ b/v7/preference/AndroidManifest.xml
@@ -1,8 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.support.v7.preference"
-    android:versionCode="1"
-    android:versionName="1.0">
+    package="android.support.v7.preference">
     <uses-sdk android:minSdkVersion="9" />
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
     <application />
 </manifest>
diff --git a/v7/preference/build.gradle b/v7/preference/build.gradle
index a63cc9e..b5fd656 100644
--- a/v7/preference/build.gradle
+++ b/v7/preference/build.gradle
@@ -15,7 +15,6 @@
  */
 
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'preference-v7'
 
 dependencies {
@@ -80,28 +79,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/recyclerview/Android.mk b/v7/recyclerview/Android.mk
index e434ab2..819b104 100644
--- a/v7/recyclerview/Android.mk
+++ b/v7/recyclerview/Android.mk
@@ -29,6 +29,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := $(call all-java-files-under,src)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_MANIFEST_FILE := AndroidManifest-make.xml
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-core-ui \
diff --git a/v7/recyclerview/AndroidManifest-make.xml b/v7/recyclerview/AndroidManifest-make.xml
new file mode 100644
index 0000000..d1c1489
--- /dev/null
+++ b/v7/recyclerview/AndroidManifest-make.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.v7.recyclerview">
+    <uses-sdk android:minSdkVersion="9"/>
+</manifest>
diff --git a/v7/recyclerview/AndroidManifest.xml b/v7/recyclerview/AndroidManifest.xml
index d1c1489..f4f010d 100644
--- a/v7/recyclerview/AndroidManifest.xml
+++ b/v7/recyclerview/AndroidManifest.xml
@@ -16,4 +16,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.support.v7.recyclerview">
     <uses-sdk android:minSdkVersion="9"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
 </manifest>
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index 11a098e1..e262ec7 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'recyclerview-v7'
 
 dependencies {
@@ -47,10 +46,6 @@
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -68,28 +63,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
index 797ace3..e6c7801 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
@@ -210,8 +210,10 @@
         int totalTaskCount = 0;
         for (int i = 0; i < viewCount; i++) {
             RecyclerView view = mRecyclerViews.get(i);
-            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
-            totalTaskCount += view.mPrefetchRegistry.mCount;
+            if (view.getWindowVisibility() == View.VISIBLE) {
+                view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
+                totalTaskCount += view.mPrefetchRegistry.mCount;
+            }
         }
 
         // Populate task list from prefetch data...
@@ -354,18 +356,23 @@
                 return;
             }
 
-            // Query last vsync so we can predict next one. Note that drawing time not yet
+            // Query most recent vsync so we can predict next one. Note that drawing time not yet
             // valid in animation/input callbacks, so query it here to be safe.
-            long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
-                    mRecyclerViews.get(0).getDrawingTime());
-            if (lastFrameVsyncNs == 0) {
-                // abort - couldn't get last vsync for estimating next
+            final int size = mRecyclerViews.size();
+            long latestFrameVsyncMs = 0;
+            for (int i = 0; i < size; i++) {
+                RecyclerView view = mRecyclerViews.get(i);
+                if (view.getWindowVisibility() == View.VISIBLE) {
+                    latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
+                }
+            }
+
+            if (latestFrameVsyncMs == 0) {
+                // abort - either no views visible, or couldn't get last vsync for estimating next
                 return;
             }
 
-            // TODO: consider rebasing deadline if frame was already dropped due to long UI work.
-            // Next frame will still wait for VSYNC, so we can still use the gap if it exists.
-            long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
+            long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
 
             prefetch(nextFrameNs);
 
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index 0b4ccc0..64df96c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -511,7 +511,7 @@
         int count = 0;
         while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
             final int pos = layoutState.mCurrentPosition;
-            layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+            layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
             final int spanSize = mSpanSizeLookup.getSpanSize(pos);
             remainingSpan -= spanSize;
             layoutState.mCurrentPosition += layoutState.mItemDirection;
@@ -693,7 +693,7 @@
             if (params.isItemRemoved() || params.isItemChanged()) {
                 result.mIgnoreConsumed = true;
             }
-            result.mFocusable |= view.isFocusable();
+            result.mFocusable |= view.hasFocusable();
         }
         Arrays.fill(mSet, null);
     }
@@ -1017,47 +1017,98 @@
             limit = getChildCount();
         }
         final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
-        View weakCandidate = null; // somewhat matches but not strong
-        int weakCandidateSpanIndex = -1;
-        int weakCandidateOverlap = 0; // how many spans overlap
 
+        // The focusable candidate to be picked if no perfect focusable candidate is found.
+        // The best focusable candidate is the one with the highest amount of span overlap with
+        // the currently focused view.
+        View focusableWeakCandidate = null; // somewhat matches but not strong
+        int focusableWeakCandidateSpanIndex = -1;
+        int focusableWeakCandidateOverlap = 0; // how many spans overlap
+
+        // The unfocusable candidate to become visible on the screen next, if no perfect or
+        // weak focusable candidates are found to receive focus next.
+        // We are only interested in partially visible unfocusable views. These are views that are
+        // not fully visible, that is either partially overlapping, or out-of-bounds and right below
+        // or above RV's padded bounded area. The best unfocusable candidate is the one with the
+        // highest amount of span overlap with the currently focused view.
+        View unfocusableWeakCandidate = null; // somewhat matches but not strong
+        int unfocusableWeakCandidateSpanIndex = -1;
+        int unfocusableWeakCandidateOverlap = 0; // how many spans overlap
+
+        // The span group index of the start child. This indicates the span group index of the
+        // next focusable item to receive focus, if a focusable item within the same span group
+        // exists. Any focusable item beyond this group index are not relevant since they
+        // were already stored in the layout before onFocusSearchFailed call and were not picked
+        // by the focusSearch algorithm.
+        int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start);
         for (int i = start; i != limit; i += inc) {
+            int spanGroupIndex = getSpanGroupIndex(recycler, state, i);
             View candidate = getChildAt(i);
             if (candidate == prevFocusedChild) {
                 break;
             }
-            if (!candidate.isFocusable()) {
+
+            if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
+                // We are past the allowable span group index for the next focusable item.
+                // The search only continues if no focusable weak candidates have been found up
+                // until this point, in order to find the best unfocusable candidate to become
+                // visible on the screen next.
+                if (focusableWeakCandidate != null) {
+                    break;
+                }
                 continue;
             }
+
             final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams();
             final int candidateStart = candidateLp.mSpanIndex;
             final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize;
-            if (candidateStart == prevSpanStart && candidateEnd == prevSpanEnd) {
+            if (candidate.hasFocusable() && candidateStart == prevSpanStart
+                    && candidateEnd == prevSpanEnd) {
                 return candidate; // perfect match
             }
             boolean assignAsWeek = false;
-            if (weakCandidate == null) {
+            if ((candidate.hasFocusable() && focusableWeakCandidate == null)
+                    || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) {
                 assignAsWeek = true;
             } else {
                 int maxStart = Math.max(candidateStart, prevSpanStart);
                 int minEnd = Math.min(candidateEnd, prevSpanEnd);
                 int overlap = minEnd - maxStart;
-                if (overlap > weakCandidateOverlap) {
-                    assignAsWeek = true;
-                } else if (overlap == weakCandidateOverlap &&
-                        preferLastSpan == (candidateStart > weakCandidateSpanIndex)) {
-                    assignAsWeek = true;
+                if (candidate.hasFocusable()) {
+                    if (overlap > focusableWeakCandidateOverlap) {
+                        assignAsWeek = true;
+                    } else if (overlap == focusableWeakCandidateOverlap
+                            && preferLastSpan == (candidateStart
+                            > focusableWeakCandidateSpanIndex)) {
+                        assignAsWeek = true;
+                    }
+                } else if (focusableWeakCandidate == null
+                        && isViewPartiallyVisible(candidate, false, true)) {
+                    if (overlap > unfocusableWeakCandidateOverlap) {
+                        assignAsWeek = true;
+                    } else if (overlap == unfocusableWeakCandidateOverlap
+                            && preferLastSpan == (candidateStart
+                                    > unfocusableWeakCandidateSpanIndex)) {
+                        assignAsWeek = true;
+                    }
                 }
             }
 
             if (assignAsWeek) {
-                weakCandidate = candidate;
-                weakCandidateSpanIndex = candidateLp.mSpanIndex;
-                weakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) -
-                        Math.max(candidateStart, prevSpanStart);
+                if (candidate.hasFocusable()) {
+                    focusableWeakCandidate = candidate;
+                    focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
+                    focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
+                            - Math.max(candidateStart, prevSpanStart);
+                } else {
+                    unfocusableWeakCandidate = candidate;
+                    unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
+                    unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
+                            - Math.max(candidateStart, prevSpanStart);
+                }
             }
         }
-        return weakCandidate;
+        return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate;
     }
 
     @Override
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 78a31b4..ac14206 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -18,12 +18,14 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
+import static android.support.v7.widget.RecyclerView.VERBOSE_TRACING;
 
 import android.content.Context;
 import android.graphics.PointF;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.support.annotation.RestrictTo;
+import android.support.v4.os.TraceCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
@@ -674,7 +676,8 @@
      * If necessary, layouts new items for predictive animations
      */
     private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
-            RecyclerView.State state, int startOffset,  int endOffset) {
+            RecyclerView.State state, int startOffset,
+            int endOffset) {
         // If there are scrap children that we did not layout, we need to find where they did go
         // and layout them accordingly so that animations can work as expected.
         // This case may happen if new views are added or an existing view expands and pushes
@@ -854,7 +857,7 @@
                 }
                 anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
                         ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper
-                                .getTotalSpaceChange())
+                        .getTotalSpaceChange())
                         : mOrientationHelper.getDecoratedStart(child);
             } else { // item is not visible.
                 if (getChildCount() > 0) {
@@ -1199,7 +1202,7 @@
             LayoutPrefetchRegistry layoutPrefetchRegistry) {
         final int pos = layoutState.mCurrentPosition;
         if (pos >= 0 && pos < state.getItemCount()) {
-            layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+            layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
         }
     }
 
@@ -1493,7 +1496,13 @@
         LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
         while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
             layoutChunkResult.resetInternal();
+            if (VERBOSE_TRACING) {
+                TraceCompat.beginSection("LLM LayoutChunk");
+            }
             layoutChunk(recycler, state, layoutState, layoutChunkResult);
+            if (VERBOSE_TRACING) {
+                TraceCompat.endSection();
+            }
             if (layoutChunkResult.mFinished) {
                 break;
             }
@@ -1598,7 +1607,7 @@
         if (params.isItemRemoved() || params.isItemChanged()) {
             result.mIgnoreConsumed = true;
         }
-        result.mFocusable = view.isFocusable();
+        result.mFocusable = view.hasFocusable();
     }
 
     @Override
@@ -1785,6 +1794,32 @@
         return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
     }
 
+    // returns the out-of-bound child view closest to RV's end bounds. An out-of-bound child is
+    // defined as a child that's either partially or fully invisible (outside RV's padding area).
+    private View findPartiallyOrCompletelyInvisibleChildClosestToEnd(RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return mShouldReverseLayout ? findFirstPartiallyOrCompletelyInvisibleChild(recycler, state)
+                : findLastPartiallyOrCompletelyInvisibleChild(recycler, state);
+    }
+
+    // returns the out-of-bound child view closest to RV's starting bounds. An out-of-bound child is
+    // defined as a child that's either partially or fully invisible (outside RV's padding area).
+    private View findPartiallyOrCompletelyInvisibleChildClosestToStart(
+            RecyclerView.Recycler recycler, RecyclerView.State state) {
+        return mShouldReverseLayout ? findLastPartiallyOrCompletelyInvisibleChild(recycler, state) :
+                findFirstPartiallyOrCompletelyInvisibleChild(recycler, state);
+    }
+
+    private View findFirstPartiallyOrCompletelyInvisibleChild(RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return findOnePartiallyOrCompletelyInvisibleChild(0, getChildCount());
+    }
+
+    private View findLastPartiallyOrCompletelyInvisibleChild(RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return findOnePartiallyOrCompletelyInvisibleChild(getChildCount() - 1, -1);
+    }
+
     /**
      * Returns the adapter position of the first visible view. This position does not include
      * adapter changes that were dispatched after the last layout pass.
@@ -1865,30 +1900,58 @@
         return child == null ? NO_POSITION : getPosition(child);
     }
 
+    // Returns the first child that is visible in the provided index range, i.e. either partially or
+    // fully visible depending on the arguments provided. Completely invisible children are not
+    // acceptable by this method, but could be returned
+    // using #findOnePartiallyOrCompletelyInvisibleChild
     View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible,
             boolean acceptPartiallyVisible) {
         ensureLayoutState();
-        final int start = mOrientationHelper.getStartAfterPadding();
-        final int end = mOrientationHelper.getEndAfterPadding();
-        final int next = toIndex > fromIndex ? 1 : -1;
-        View partiallyVisible = null;
-        for (int i = fromIndex; i != toIndex; i+=next) {
-            final View child = getChildAt(i);
-            final int childStart = mOrientationHelper.getDecoratedStart(child);
-            final int childEnd = mOrientationHelper.getDecoratedEnd(child);
-            if (childStart < end && childEnd > start) {
-                if (completelyVisible) {
-                    if (childStart >= start && childEnd <= end) {
-                        return child;
-                    } else if (acceptPartiallyVisible && partiallyVisible == null) {
-                        partiallyVisible = child;
-                    }
-                } else {
-                    return child;
-                }
-            }
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0;
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0;
+        if (completelyVisible) {
+            preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS
+                    | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE);
+        } else {
+            preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                    | ViewBoundsCheck.FLAG_CVE_GT_PVS);
         }
-        return partiallyVisible;
+        if (acceptPartiallyVisible) {
+            acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                    | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        }
+        return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck
+                .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag,
+                        acceptableBoundsFlag) : mVerticalBoundCheck
+                .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag,
+                        acceptableBoundsFlag);
+    }
+
+    View findOnePartiallyOrCompletelyInvisibleChild(int fromIndex, int toIndex) {
+        ensureLayoutState();
+        final int next = toIndex > fromIndex ? 1 : (toIndex < fromIndex ? -1 : 0);
+        if (next == 0) {
+            return getChildAt(fromIndex);
+        }
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0;
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0;
+        if (mOrientationHelper.getDecoratedStart(getChildAt(fromIndex))
+                < mOrientationHelper.getStartAfterPadding()) {
+            preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE
+                    | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+            acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS
+                    | ViewBoundsCheck.FLAG_CVE_LT_PVE);
+        } else {
+            preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE | ViewBoundsCheck.FLAG_CVS_GT_PVS
+                    | ViewBoundsCheck.FLAG_CVS_LT_PVE);
+            acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE
+                    | ViewBoundsCheck.FLAG_CVS_GT_PVS);
+        }
+        return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck
+                .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag,
+                        acceptableBoundsFlag) : mVerticalBoundCheck
+                .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag,
+                        acceptableBoundsFlag);
     }
 
     @Override
@@ -1904,35 +1967,38 @@
             return null;
         }
         ensureLayoutState();
-        final View referenceChild;
-        if (layoutDir == LayoutState.LAYOUT_START) {
-            referenceChild = findReferenceChildClosestToStart(recycler, state);
-        } else {
-            referenceChild = findReferenceChildClosestToEnd(recycler, state);
-        }
-        if (referenceChild == null) {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "Cannot find a child with a valid position to be used for focus search.");
-            }
-            return null;
-        }
         ensureLayoutState();
         final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
         updateLayoutState(layoutDir, maxScroll, false, state);
         mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
         mLayoutState.mRecycle = false;
         fill(recycler, mLayoutState, state, true);
+
+        // nextCandidate is the first child view in the layout direction that's partially
+        // within RV's bounds, i.e. part of it is visible or it's completely invisible but still
+        // touching RV's bounds. This will be the unfocusable candidate view to become visible onto
+        // the screen if no focusable views are found in the given layout direction.
+        final View nextCandidate;
+        if (layoutDir == LayoutState.LAYOUT_START) {
+            nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToStart(recycler, state);
+        } else {
+            nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToEnd(recycler, state);
+        }
+        // nextFocus is meaningful only if it refers to a focusable child, in which case it
+        // indicates the next view to gain focus.
         final View nextFocus;
         if (layoutDir == LayoutState.LAYOUT_START) {
             nextFocus = getChildClosestToStart();
         } else {
             nextFocus = getChildClosestToEnd();
         }
-        if (nextFocus == referenceChild || !nextFocus.isFocusable()) {
-            return null;
+        if (nextFocus.hasFocusable()) {
+            if (nextCandidate == null) {
+                return null;
+            }
+            return nextFocus;
         }
-        return nextFocus;
+        return nextCandidate;
     }
 
     /**
@@ -2020,9 +2086,9 @@
         if (mShouldReverseLayout) {
             if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) {
                 scrollToPositionWithOffset(targetPos,
-                        mOrientationHelper.getEndAfterPadding() -
-                                (mOrientationHelper.getDecoratedStart(target) +
-                                mOrientationHelper.getDecoratedMeasurement(view)));
+                        mOrientationHelper.getEndAfterPadding()
+                                - (mOrientationHelper.getDecoratedStart(target)
+                                + mOrientationHelper.getDecoratedMeasurement(view)));
             } else {
                 scrollToPositionWithOffset(targetPos,
                         mOrientationHelper.getEndAfterPadding() -
diff --git a/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
index 3190f91..953e091 100644
--- a/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
@@ -29,9 +29,10 @@
  * <p>
  *
  * PagerSnapHelper can help achieve a similar behavior to {@link android.support.v4.view.ViewPager}.
- * Set both {@link RecyclerView} and {@link android.support.v7.widget.RecyclerView.Adapter} to have
- * MATCH_PARENT height and width and then attach PagerSnapHelper to the {@link RecyclerView} using
- * {@link #attachToRecyclerView(RecyclerView)}.
+ * Set both {@link RecyclerView} and the items of the
+ * {@link android.support.v7.widget.RecyclerView.Adapter} to have
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach
+ * PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}.
  */
 public class PagerSnapHelper extends SnapHelper {
     private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index f70b1ec..5f06408 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -154,6 +154,8 @@
 
     static final boolean DEBUG = false;
 
+    static final boolean VERBOSE_TRACING = false;
+
     private static final int[]  NESTED_SCROLLING_ATTRS
             = {16843830 /* android.R.attr.nestedScrollingEnabled */};
 
@@ -507,38 +509,39 @@
      */
     private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
             new ViewInfoStore.ProcessCallback() {
-        @Override
-        public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
-                @Nullable ItemHolderInfo postInfo) {
-            mRecycler.unscrapView(viewHolder);
-            animateDisappearance(viewHolder, info, postInfo);
-        }
-        @Override
-        public void processAppeared(ViewHolder viewHolder,
-                ItemHolderInfo preInfo, ItemHolderInfo info) {
-            animateAppearance(viewHolder, preInfo, info);
-        }
-
-        @Override
-        public void processPersistent(ViewHolder viewHolder,
-                @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
-            viewHolder.setIsRecyclable(false);
-            if (mDataSetHasChangedAfterLayout) {
-                // since it was rebound, use change instead as we'll be mapping them from
-                // stable ids. If stable ids were false, we would not be running any
-                // animations
-                if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
-                    postAnimationRunner();
+                @Override
+                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
+                        @Nullable ItemHolderInfo postInfo) {
+                    mRecycler.unscrapView(viewHolder);
+                    animateDisappearance(viewHolder, info, postInfo);
                 }
-            } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
-                postAnimationRunner();
-            }
-        }
-        @Override
-        public void unused(ViewHolder viewHolder) {
-            mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
-        }
-    };
+                @Override
+                public void processAppeared(ViewHolder viewHolder,
+                        ItemHolderInfo preInfo, ItemHolderInfo info) {
+                    animateAppearance(viewHolder, preInfo, info);
+                }
+
+                @Override
+                public void processPersistent(ViewHolder viewHolder,
+                        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+                    viewHolder.setIsRecyclable(false);
+                    if (mDataSetHasChangedAfterLayout) {
+                        // since it was rebound, use change instead as we'll be mapping them from
+                        // stable ids. If stable ids were false, we would not be running any
+                        // animations
+                        if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo,
+                                postInfo)) {
+                            postAnimationRunner();
+                        }
+                    } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
+                        postAnimationRunner();
+                    }
+                }
+                @Override
+                public void unused(ViewHolder viewHolder) {
+                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
+                }
+            };
 
     public RecyclerView(Context context) {
         this(context, null);
@@ -702,7 +705,13 @@
 
             @Override
             public void addView(View child, int index) {
+                if (VERBOSE_TRACING) {
+                    TraceCompat.beginSection("RV addView");
+                }
                 RecyclerView.this.addView(child, index);
+                if (VERBOSE_TRACING) {
+                    TraceCompat.endSection();
+                }
                 dispatchChildAttached(child);
             }
 
@@ -717,7 +726,13 @@
                 if (child != null) {
                     dispatchChildDetached(child);
                 }
+                if (VERBOSE_TRACING) {
+                    TraceCompat.beginSection("RV removeViewAt");
+                }
                 RecyclerView.this.removeViewAt(index);
+                if (VERBOSE_TRACING) {
+                    TraceCompat.endSection();
+                }
             }
 
             @Override
@@ -2318,6 +2333,19 @@
                 resumeRequestLayout(false);
             }
         }
+        if (result != null && !result.hasFocusable()) {
+            if (getFocusedChild() == null) {
+                // Scrolling to this unfocusable view is not meaningful since there is no currently
+                // focused view which RV needs to keep visible.
+                return super.focusSearch(focused, direction);
+            }
+            // If the next view returned by onFocusSearchFailed in layout manager has no focusable
+            // views, we still scroll to that view in order to make it visible on the screen.
+            // If it's focusable, framework already calls RV's requestChildFocus which handles
+            // bringing this newly focused item onto the screen.
+            requestChildOnScreen(result, null);
+            return focused;
+        }
         return isPreferredNextFocus(focused, result, direction)
                 ? result : super.focusSearch(focused, direction);
     }
@@ -2388,31 +2416,48 @@
     @Override
     public void requestChildFocus(View child, View focused) {
         if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
-            mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
-
-            // get item decor offsets w/o refreshing. If they are invalid, there will be another
-            // layout pass to fix them, then it is LayoutManager's responsibility to keep focused
-            // View in viewport.
-            final ViewGroup.LayoutParams focusedLayoutParams = focused.getLayoutParams();
-            if (focusedLayoutParams instanceof LayoutParams) {
-                // if focused child has item decors, use them. Otherwise, ignore.
-                final LayoutParams lp = (LayoutParams) focusedLayoutParams;
-                if (!lp.mInsetsDirty) {
-                    final Rect insets = lp.mDecorInsets;
-                    mTempRect.left -= insets.left;
-                    mTempRect.right += insets.right;
-                    mTempRect.top -= insets.top;
-                    mTempRect.bottom += insets.bottom;
-                }
-            }
-
-            offsetDescendantRectToMyCoords(focused, mTempRect);
-            offsetRectIntoDescendantCoords(child, mTempRect);
-            requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete);
+            requestChildOnScreen(child, focused);
         }
         super.requestChildFocus(child, focused);
     }
 
+    /**
+     * Requests that the given child of the RecyclerView be positioned onto the screen. This method
+     * can be called for both unfocusable and focusable child views. For unfocusable child views,
+     * the {@param focused} parameter passed is null, whereas for a focusable child, this parameter
+     * indicates the actual descendant view within this child view that holds the focus.
+     * @param child The child view of this RecyclerView that wants to come onto the screen.
+     * @param focused The descendant view that actually has the focus if child is focusable, null
+     *                otherwise.
+     */
+    private void requestChildOnScreen(@NonNull View child, @Nullable View focused) {
+        View rectView = (focused != null) ? focused : child;
+        mTempRect.set(0, 0, rectView.getWidth(), rectView.getHeight());
+
+        // get item decor offsets w/o refreshing. If they are invalid, there will be another
+        // layout pass to fix them, then it is LayoutManager's responsibility to keep focused
+        // View in viewport.
+        final ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams();
+        if (focusedLayoutParams instanceof LayoutParams) {
+            // if focused child has item decors, use them. Otherwise, ignore.
+            final LayoutParams lp = (LayoutParams) focusedLayoutParams;
+            if (!lp.mInsetsDirty) {
+                final Rect insets = lp.mDecorInsets;
+                mTempRect.left -= insets.left;
+                mTempRect.right += insets.right;
+                mTempRect.top -= insets.top;
+                mTempRect.bottom += insets.bottom;
+            }
+        }
+
+        if (focused != null) {
+            offsetDescendantRectToMyCoords(focused, mTempRect);
+            offsetRectIntoDescendantCoords(child, mTempRect);
+        }
+        mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete,
+                (focused == null));
+    }
+
     @Override
     public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
         return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
@@ -2533,10 +2578,11 @@
             throw new IllegalStateException(message);
         }
         if (mDispatchScrollCounter > 0) {
-            Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might be run"
-                    + " during a measure & layout pass where you cannot change the RecyclerView"
-                    + " data. Any method call that might change the structure of the RecyclerView"
-                    + " or the adapter contents should be postponed to the next frame.",
+            Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might"
+                            + "be run during a measure & layout pass where you cannot change the"
+                            + "RecyclerView data. Any method call that might change the structure"
+                            + "of the RecyclerView or the adapter contents should be postponed to"
+                            + "the next frame.",
                     new IllegalStateException(""));
         }
     }
@@ -3213,10 +3259,10 @@
         mState.mRunSimpleAnimations = mFirstLayoutComplete
                 && mItemAnimator != null
                 && (mDataSetHasChangedAfterLayout
-                        || animationTypeSupported
-                        || mLayout.mRequestedSimpleAnimations)
+                || animationTypeSupported
+                || mLayout.mRequestedSimpleAnimations)
                 && (!mDataSetHasChangedAfterLayout
-                        || mAdapter.hasStableIds());
+                || mAdapter.hasStableIds());
         mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                 && animationTypeSupported
                 && !mDataSetHasChangedAfterLayout
@@ -5714,9 +5760,9 @@
             if (forceRecycle || holder.isRecyclable()) {
                 if (mViewCacheMax > 0
                         && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
-                                | ViewHolder.FLAG_REMOVED
-                                | ViewHolder.FLAG_UPDATE
-                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
+                        | ViewHolder.FLAG_REMOVED
+                        | ViewHolder.FLAG_UPDATE
+                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                     // Retire oldest cached view
                     int cachedViewSize = mCachedViews.size();
                     if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
@@ -6816,6 +6862,109 @@
         ChildHelper mChildHelper;
         RecyclerView mRecyclerView;
 
+        /**
+         * The callback used for retrieving information about a RecyclerView and its children in the
+         * horizontal direction.
+         */
+        private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback =
+                new ViewBoundsCheck.Callback() {
+                    @Override
+                    public int getChildCount() {
+                        return LayoutManager.this.getChildCount();
+                    }
+
+                    @Override
+                    public View getParent() {
+                        return mRecyclerView;
+                    }
+
+                    @Override
+                    public View getChildAt(int index) {
+                        return LayoutManager.this.getChildAt(index);
+                    }
+
+                    @Override
+                    public int getParentStart() {
+                        return LayoutManager.this.getPaddingLeft();
+                    }
+
+                    @Override
+                    public int getParentEnd() {
+                        return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight();
+                    }
+
+                    @Override
+                    public int getChildStart(View view) {
+                        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                                view.getLayoutParams();
+                        return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin;
+                    }
+
+                    @Override
+                    public int getChildEnd(View view) {
+                        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                                view.getLayoutParams();
+                        return LayoutManager.this.getDecoratedRight(view) + params.rightMargin;
+                    }
+                };
+
+        /**
+         * The callback used for retrieving information about a RecyclerView and its children in the
+         * vertical direction.
+         */
+        private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback =
+                new ViewBoundsCheck.Callback() {
+                    @Override
+                    public int getChildCount() {
+                        return LayoutManager.this.getChildCount();
+                    }
+
+                    @Override
+                    public View getParent() {
+                        return mRecyclerView;
+                    }
+
+                    @Override
+                    public View getChildAt(int index) {
+                        return LayoutManager.this.getChildAt(index);
+                    }
+
+                    @Override
+                    public int getParentStart() {
+                        return LayoutManager.this.getPaddingTop();
+                    }
+
+                    @Override
+                    public int getParentEnd() {
+                        return LayoutManager.this.getHeight()
+                                - LayoutManager.this.getPaddingBottom();
+                    }
+
+                    @Override
+                    public int getChildStart(View view) {
+                        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                                view.getLayoutParams();
+                        return LayoutManager.this.getDecoratedTop(view) - params.topMargin;
+                    }
+
+                    @Override
+                    public int getChildEnd(View view) {
+                        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                                view.getLayoutParams();
+                        return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin;
+                    }
+                };
+
+        /**
+         * Utility objects used to check the boundaries of children against their parent
+         * RecyclerView.
+         * @see #isViewPartiallyVisible(View, boolean, boolean),
+         * {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)},
+         * and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}.
+         */
+        ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback);
+        ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback);
+
         @Nullable
         SmoothScroller mSmoothScroller;
 
@@ -8880,7 +9029,11 @@
          *
          * <p>This is the LayoutManager's opportunity to populate views in the given direction
          * to fulfill the request if it can. The LayoutManager should attach and return
-         * the view to be focused. The default implementation returns null.</p>
+         * the view to be focused, if a focusable view in the given direction is found.
+         * Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns
+         * the next unfocusable view to become visible on the screen. This unfocusable view is
+         * typically the first view that's either partially or fully out of RV's padded bounded
+         * area in the given direction. The default implementation returns null.</p>
          *
          * @param focused   The currently focused view
          * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
@@ -8889,7 +9042,8 @@
          *                  or 0 for not applicable
          * @param recycler  The recycler to use for obtaining views for currently offscreen items
          * @param state     Transient state of RecyclerView
-         * @return The chosen view to be focused
+         * @return The chosen view to be focused if a focusable view is found, otherwise an
+         * unfocusable view to become visible onto the screen, else null.
          */
         @Nullable
         public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
@@ -8918,22 +9072,20 @@
         }
 
         /**
-         * Called when a child of the RecyclerView wants a particular rectangle to be positioned
-         * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
-         * android.graphics.Rect, boolean)} for more details.
-         *
-         * <p>The base implementation will attempt to perform a standard programmatic scroll
-         * to bring the given rect into view, within the padded area of the RecyclerView.</p>
-         *
+         * Returns the scroll amount that brings the given rect in child's coordinate system within
+         * the padded area of RecyclerView.
+         * @param parent The parent RecyclerView.
          * @param child The direct child making the request.
-         * @param rect  The rectangle in the child's coordinates the child
-         *              wishes to be on the screen.
+         * @param rect The rectangle in the child's coordinates the child
+         *             wishes to be on the screen.
          * @param immediate True to forbid animated or delayed scrolling,
          *                  false otherwise
-         * @return Whether the group scrolled to handle the operation
+         * @return The array containing the scroll amount in x and y directions that brings the
+         * given rect into RV's padded area.
          */
-        public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
-                boolean immediate) {
+        private int[] getChildRectangleOnScreenScrollAmount(RecyclerView parent, View child,
+                Rect rect, boolean immediate) {
+            int[] out = new int[2];
             final int parentLeft = getPaddingLeft();
             final int parentTop = getPaddingTop();
             final int parentRight = getWidth() - getPaddingRight();
@@ -8964,19 +9116,125 @@
             // we should scroll to make bottom visible, make sure top does not go out of bounds.
             final int dy = offScreenTop != 0 ? offScreenTop
                     : Math.min(childTop - parentTop, offScreenBottom);
+            out[0] = dx;
+            out[1] = dy;
+            return out;
+        }
+        /**
+         * Called when a child of the RecyclerView wants a particular rectangle to be positioned
+         * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
+         * android.graphics.Rect, boolean)} for more details.
+         *
+         * <p>The base implementation will attempt to perform a standard programmatic scroll
+         * to bring the given rect into view, within the padded area of the RecyclerView.</p>
+         *
+         * @param child The direct child making the request.
+         * @param rect  The rectangle in the child's coordinates the child
+         *              wishes to be on the screen.
+         * @param immediate True to forbid animated or delayed scrolling,
+         *                  false otherwise
+         * @return Whether the group scrolled to handle the operation
+         */
+        public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
+                boolean immediate) {
+            return requestChildRectangleOnScreen(parent, child, rect, immediate, false);
+        }
 
-            if (dx != 0 || dy != 0) {
-                if (immediate) {
-                    parent.scrollBy(dx, dy);
-                } else {
-                    parent.smoothScrollBy(dx, dy);
+        /**
+         * Requests that the given child of the RecyclerView be positioned onto the screen. This
+         * method can be called for both unfocusable and focusable child views. For unfocusable
+         * child views, focusedChildVisible is typically true in which case, layout manager
+         * makes the child view visible only if the currently focused child stays in-bounds of RV.
+         * @param parent The parent RecyclerView.
+         * @param child The direct child making the request.
+         * @param rect The rectangle in the child's coordinates the child
+         *              wishes to be on the screen.
+         * @param immediate True to forbid animated or delayed scrolling,
+         *                  false otherwise
+         * @param focusedChildVisible Whether the currently focused view must stay visible.
+         * @return Whether the group scrolled to handle the operation
+         */
+        public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
+                boolean immediate,
+                boolean focusedChildVisible) {
+            int[] scrollAmount = getChildRectangleOnScreenScrollAmount(parent, child, rect,
+                    immediate);
+            int dx = scrollAmount[0];
+            int dy = scrollAmount[1];
+            if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) {
+                if (dx != 0 || dy != 0) {
+                    if (immediate) {
+                        parent.scrollBy(dx, dy);
+                    } else {
+                        parent.smoothScrollBy(dx, dy);
+                    }
+                    return true;
                 }
-                return true;
             }
             return false;
         }
 
         /**
+         * Returns whether the given child view is partially or fully visible within the padded
+         * bounded area of RecyclerView, depending on the input parameters.
+         * A view is partially visible if it has non-zero overlap with RV's padded bounded area.
+         * If acceptEndPointInclusion flag is set to true, it's also considered partially
+         * visible if it's located outside RV's bounds and it's hitting either RV's start or end
+         * bounds.
+         *
+         * @param child The child view to be examined.
+         * @param completelyVisible If true, the method returns true iff the child is completely
+         *                          visible. If false, the method returns true iff the child is only
+         *                          partially visible (that is it will return false if the child is
+         *                          either completely visible or out of RV's bounds).
+         * @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end
+         *                                bounds is enough to consider it partially visible,
+         *                                false otherwise.
+         * @return True if the given child is partially or fully visible, false otherwise.
+         */
+        public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible,
+                boolean acceptEndPointInclusion) {
+            int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS
+                    | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE);
+            boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child,
+                    boundsFlag)
+                    && mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag);
+            if (completelyVisible) {
+                return isViewFullyVisible;
+            } else {
+                return !isViewFullyVisible;
+            }
+        }
+
+        /**
+         * Returns whether the currently focused child stays within RV's bounds with the given
+         * amount of scrolling.
+         * @param parent The parent RecyclerView.
+         * @param dx The scrolling in x-axis direction to be performed.
+         * @param dy The scrolling in y-axis direction to be performed.
+         * @return {@code false} if the focused child is not at least partially visible after
+         *         scrolling or no focused child exists, {@code true} otherwise.
+         */
+        private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) {
+            final View focusedChild = parent.getFocusedChild();
+            if (focusedChild == null) {
+                return false;
+            }
+            final int parentLeft = getPaddingLeft();
+            final int parentTop = getPaddingTop();
+            final int parentRight = getWidth() - getPaddingRight();
+            final int parentBottom = getHeight() - getPaddingBottom();
+            final Rect bounds = mRecyclerView.mTempRect;
+            getDecoratedBoundsWithMargins(focusedChild, bounds);
+
+            if (bounds.left - dx >= parentRight || bounds.right - dx <= parentLeft
+                    || bounds.top - dy >= parentBottom || bounds.bottom - dy <= parentTop) {
+                return false;
+            }
+            return true;
+        }
+
+        /**
          * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)}
          */
         @Deprecated
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 19f31f8..bbf4acd 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -1672,7 +1672,7 @@
                 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
             }
             recycle(recycler, mLayoutState);
-            if (mLayoutState.mStopInFocusable && view.isFocusable()) {
+            if (mLayoutState.mStopInFocusable && view.hasFocusable()) {
                 if (lp.mFullSpan) {
                     mRemainingSpans.clear();
                 } else {
@@ -2284,6 +2284,7 @@
                 return view;
             }
         }
+
         // either could not find from the desired span or prev view is full span.
         // traverse all spans
         if (preferLastSpan(layoutDir)) {
@@ -2301,6 +2302,44 @@
                 }
             }
         }
+
+        // Could not find any focusable views from any of the existing spans. Now start the search
+        // to find the best unfocusable candidate to become visible on the screen next. The search
+        // is done in the same fashion: first, check the views in the desired span and if no
+        // candidate is found, traverse the views in all the remaining spans.
+        boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START);
+        View unfocusableCandidate = null;
+        if (!prevFocusFullSpan) {
+            unfocusableCandidate = findViewByPosition(shouldSearchFromStart
+                    ? prevFocusSpan.findFirstPartiallyVisibleItemPosition() :
+                    prevFocusSpan.findLastPartiallyVisibleItemPosition());
+            if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
+                return unfocusableCandidate;
+            }
+        }
+
+        if (preferLastSpan(layoutDir)) {
+            for (int i = mSpanCount - 1; i >= 0; i--) {
+                if (i == prevFocusSpan.mIndex) {
+                    continue;
+                }
+                unfocusableCandidate = findViewByPosition(shouldSearchFromStart
+                        ? mSpans[i].findFirstPartiallyVisibleItemPosition() :
+                        mSpans[i].findLastPartiallyVisibleItemPosition());
+                if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
+                    return unfocusableCandidate;
+                }
+            }
+        } else {
+            for (int i = 0; i < mSpanCount; i++) {
+                unfocusableCandidate = findViewByPosition(shouldSearchFromStart
+                        ? mSpans[i].findFirstPartiallyVisibleItemPosition() :
+                        mSpans[i].findLastPartiallyVisibleItemPosition());
+                if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
+                    return unfocusableCandidate;
+                }
+            }
+        }
         return null;
     }
 
@@ -2622,6 +2661,12 @@
                     : findOneVisibleChild(0, mViews.size(), false);
         }
 
+        public int findFirstPartiallyVisibleItemPosition() {
+            return mReverseLayout
+                    ? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true)
+                    : findOnePartiallyVisibleChild(0, mViews.size(), true);
+        }
+
         public int findFirstCompletelyVisibleItemPosition() {
             return mReverseLayout
                     ? findOneVisibleChild(mViews.size() - 1, -1, true)
@@ -2634,13 +2679,45 @@
                     : findOneVisibleChild(mViews.size() - 1, -1, false);
         }
 
+        public int findLastPartiallyVisibleItemPosition() {
+            return mReverseLayout
+                    ? findOnePartiallyVisibleChild(0, mViews.size(), true)
+                    : findOnePartiallyVisibleChild(mViews.size() - 1, -1, true);
+        }
+
         public int findLastCompletelyVisibleItemPosition() {
             return mReverseLayout
                     ? findOneVisibleChild(0, mViews.size(), true)
                     : findOneVisibleChild(mViews.size() - 1, -1, true);
         }
 
-        int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
+        /**
+         * Returns the first view within this span that is partially or fully visible. Partially
+         * visible refers to a view that overlaps but is not fully contained within RV's padded
+         * bounded area. This view returned can be defined to have an area of overlap strictly
+         * greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint
+         * inclusion is enough to consider it partially visible. The latter case can then refer to
+         * an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded
+         * area. This is used e.g. inside
+         * {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for
+         * calculating the next unfocusable child to become visible on the screen.
+         * @param fromIndex The child position index to start the search from.
+         * @param toIndex The child position index to end the search at.
+         * @param completelyVisible True if we have to only consider completely visible views,
+         *                          false otherwise.
+         * @param acceptCompletelyVisible True if we can consider both partially or fully visible
+         *                                views, false, if only a partially visible child should be
+         *                                returned.
+         * @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded
+         *                                bounded area is enough to consider it partially visible,
+         *                                false otherwise
+         * @return The adapter position of the first view that's either partially or fully visible.
+         * {@link RecyclerView#NO_POSITION} if no such view is found.
+         */
+        int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex,
+                boolean completelyVisible,
+                boolean acceptCompletelyVisible,
+                boolean acceptEndPointInclusion) {
             final int start = mPrimaryOrientation.getStartAfterPadding();
             final int end = mPrimaryOrientation.getEndAfterPadding();
             final int next = toIndex > fromIndex ? 1 : -1;
@@ -2648,12 +2725,22 @@
                 final View child = mViews.get(i);
                 final int childStart = mPrimaryOrientation.getDecoratedStart(child);
                 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
-                if (childStart < end && childEnd > start) {
-                    if (completelyVisible) {
+                boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end)
+                        : (childStart < end);
+                boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start)
+                        : (childEnd > start);
+                if (childStartInclusion && childEndInclusion) {
+                    if (completelyVisible && acceptCompletelyVisible) {
+                        // the child has to be completely visible to be returned.
                         if (childStart >= start && childEnd <= end) {
                             return getPosition(child);
                         }
-                    } else {
+                    } else if (acceptCompletelyVisible) {
+                        // can return either a partially or completely visible child.
+                        return getPosition(child);
+                    } else if (childStart < start || childEnd > end) {
+                        // should return a partially visible child if exists and a completely
+                        // visible child is not acceptable in this case.
                         return getPosition(child);
                     }
                 }
@@ -2661,6 +2748,17 @@
             return NO_POSITION;
         }
 
+        int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
+            return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible,
+                    true, false);
+        }
+
+        int findOnePartiallyVisibleChild(int fromIndex, int toIndex,
+                boolean acceptEndPointInclusion) {
+            return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false,
+                    acceptEndPointInclusion);
+        }
+
         /**
          * Depending on the layout direction, returns the View that is after the given position.
          */
@@ -2670,8 +2768,11 @@
                 final int limit = mViews.size();
                 for (int i = 0; i < limit; i++) {
                     final View view = mViews.get(i);
-                    if (view.isFocusable() &&
-                            (getPosition(view) > referenceChildPosition == mReverseLayout) ) {
+                    if ((mReverseLayout && getPosition(view) <= referenceChildPosition)
+                            || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) {
+                        break;
+                    }
+                    if (view.hasFocusable()) {
                         candidate = view;
                     } else {
                         break;
@@ -2680,8 +2781,11 @@
             } else {
                 for (int i = mViews.size() - 1; i >= 0; i--) {
                     final View view = mViews.get(i);
-                    if (view.isFocusable() &&
-                            (getPosition(view) > referenceChildPosition == !mReverseLayout)) {
+                    if ((mReverseLayout && getPosition(view) >= referenceChildPosition)
+                            || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) {
+                        break;
+                    }
+                    if (view.hasFocusable()) {
                         candidate = view;
                     } else {
                         break;
diff --git a/v7/recyclerview/src/android/support/v7/widget/ViewBoundsCheck.java b/v7/recyclerview/src/android/support/v7/widget/ViewBoundsCheck.java
new file mode 100644
index 0000000..8a4f89c
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/ViewBoundsCheck.java
@@ -0,0 +1,274 @@
+/*
+ * 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.annotation.IntDef;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A utility class used to check the boundaries of a given view within its parent view based on
+ * a set of boundary flags.
+ */
+class ViewBoundsCheck {
+
+    static final int GT = 1 << 0;
+    static final int EQ = 1 << 1;
+    static final int LT = 1 << 2;
+
+
+    static final int CVS_PVS_POS = 0;
+    /**
+     * The child view's start should be strictly greater than parent view's start.
+     */
+    static final int FLAG_CVS_GT_PVS = GT << CVS_PVS_POS;
+
+    /**
+     * The child view's start can be equal to its parent view's start. This flag follows with GT
+     * or LT indicating greater (less) than or equal relation.
+     */
+    static final int FLAG_CVS_EQ_PVS = EQ << CVS_PVS_POS;
+
+    /**
+     * The child view's start should be strictly less than parent view's start.
+     */
+    static final int FLAG_CVS_LT_PVS = LT << CVS_PVS_POS;
+
+
+    static final int CVS_PVE_POS = 4;
+    /**
+     * The child view's start should be strictly greater than parent view's end.
+     */
+    static final int FLAG_CVS_GT_PVE = GT << CVS_PVE_POS;
+
+    /**
+     * The child view's start can be equal to its parent view's end. This flag follows with GT
+     * or LT indicating greater (less) than or equal relation.
+     */
+    static final int FLAG_CVS_EQ_PVE = EQ << CVS_PVE_POS;
+
+    /**
+     * The child view's start should be strictly less than parent view's end.
+     */
+    static final int FLAG_CVS_LT_PVE = LT << CVS_PVE_POS;
+
+
+    static final int CVE_PVS_POS = 8;
+    /**
+     * The child view's end should be strictly greater than parent view's start.
+     */
+    static final int FLAG_CVE_GT_PVS = GT << CVE_PVS_POS;
+
+    /**
+     * The child view's end can be equal to its parent view's start. This flag follows with GT
+     * or LT indicating greater (less) than or equal relation.
+     */
+    static final int FLAG_CVE_EQ_PVS = EQ << CVE_PVS_POS;
+
+    /**
+     * The child view's end should be strictly less than parent view's start.
+     */
+    static final int FLAG_CVE_LT_PVS = LT << CVE_PVS_POS;
+
+
+    static final int CVE_PVE_POS = 12;
+    /**
+     * The child view's end should be strictly greater than parent view's end.
+     */
+    static final int FLAG_CVE_GT_PVE = GT << CVE_PVE_POS;
+
+    /**
+     * The child view's end can be equal to its parent view's end. This flag follows with GT
+     * or LT indicating greater (less) than or equal relation.
+     */
+    static final int FLAG_CVE_EQ_PVE = EQ << CVE_PVE_POS;
+
+    /**
+     * The child view's end should be strictly less than parent view's end.
+     */
+    static final int FLAG_CVE_LT_PVE = LT << CVE_PVE_POS;
+
+    static final int MASK = GT | EQ | LT;
+
+    final Callback mCallback;
+    BoundFlags mBoundFlags;
+    /**
+     * The set of flags that can be passed for checking the view boundary conditions.
+     * CVS in the flag name indicates the child view, and PV indicates the parent view.\
+     * The following S, E indicate a view's start and end points, respectively.
+     * GT and LT indicate a strictly greater and less than relationship.
+     * Greater than or equal (or less than or equal) can be specified by setting both GT and EQ (or
+     * LT and EQ) flags.
+     * For instance, setting both {@link #FLAG_CVS_GT_PVS} and {@link #FLAG_CVS_EQ_PVS} indicate the
+     * child view's start should be greater than or equal to its parent start.
+     */
+    @IntDef(flag = true, value = {
+            FLAG_CVS_GT_PVS, FLAG_CVS_EQ_PVS, FLAG_CVS_LT_PVS,
+            FLAG_CVS_GT_PVE, FLAG_CVS_EQ_PVE, FLAG_CVS_LT_PVE,
+            FLAG_CVE_GT_PVS, FLAG_CVE_EQ_PVS, FLAG_CVE_LT_PVS,
+            FLAG_CVE_EQ_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ViewBounds {}
+
+    ViewBoundsCheck(Callback callback) {
+        mCallback = callback;
+        mBoundFlags = new BoundFlags();
+    }
+
+    static class BoundFlags {
+        int mBoundFlags = 0;
+        int mRvStart, mRvEnd, mChildStart, mChildEnd;
+
+        void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) {
+            mRvStart = rvStart;
+            mRvEnd = rvEnd;
+            mChildStart = childStart;
+            mChildEnd = childEnd;
+        }
+
+        void setFlags(@ViewBounds int flags, int mask) {
+            mBoundFlags = (mBoundFlags & ~mask) | (flags & mask);
+        }
+
+        void addFlags(@ViewBounds int flags) {
+            mBoundFlags |= flags;
+        }
+
+        void resetFlags() {
+            mBoundFlags = 0;
+        }
+
+        int compare(int x, int y) {
+            if (x > y) {
+                return GT;
+            }
+            if (x == y) {
+                return EQ;
+            }
+            return LT;
+        }
+
+        boolean boundsMatch() {
+            if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) {
+                if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) {
+                    return false;
+                }
+            }
+
+            if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) {
+                if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) {
+                    return false;
+                }
+            }
+
+            if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) {
+                if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) {
+                    return false;
+                }
+            }
+
+            if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) {
+                if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    };
+
+    /**
+     * Returns the first view starting from fromIndex to toIndex in views whose bounds lie within
+     * its parent bounds based on the provided preferredBoundFlags. If no match is found based on
+     * the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose
+     * bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such
+     * view is found based on either of these two flags, null is returned.
+     * @param fromIndex The view position index to start the search from.
+     * @param toIndex The view position index to end the search at.
+     * @param preferredBoundFlags The flags indicating the preferred match. Once a match is found
+     *                            based on this flag, that view is returned instantly.
+     * @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match
+     *                             is found. If so, and if acceptableBoundFlags is non-zero, the
+     *                             last matching acceptable view is returned. Otherwise, null is
+     *                             returned.
+     * @return The first view that satisfies acceptableBoundFlags or the last view satisfying
+     * acceptableBoundFlags boundary conditions.
+     */
+    View findOneViewWithinBoundFlags(int fromIndex, int toIndex,
+            @ViewBounds int preferredBoundFlags,
+            @ViewBounds int acceptableBoundFlags) {
+        final int start = mCallback.getParentStart();
+        final int end = mCallback.getParentEnd();
+        final int next = toIndex > fromIndex ? 1 : -1;
+        View acceptableMatch = null;
+        for (int i = fromIndex; i != toIndex; i += next) {
+            final View child = mCallback.getChildAt(i);
+            final int childStart = mCallback.getChildStart(child);
+            final int childEnd = mCallback.getChildEnd(child);
+            mBoundFlags.setBounds(start, end, childStart, childEnd);
+            if (preferredBoundFlags != 0) {
+                mBoundFlags.resetFlags();
+                mBoundFlags.addFlags(preferredBoundFlags);
+                if (mBoundFlags.boundsMatch()) {
+                    // found a perfect match
+                    return child;
+                }
+            }
+            if (acceptableBoundFlags != 0) {
+                mBoundFlags.resetFlags();
+                mBoundFlags.addFlags(acceptableBoundFlags);
+                if (mBoundFlags.boundsMatch()) {
+                    acceptableMatch = child;
+                }
+            }
+        }
+        return acceptableMatch;
+    }
+
+    /**
+     * Returns whether the specified view lies within the boundary condition of its parent view.
+     * @param child The child view to be checked.
+     * @param boundsFlags The flag against which the child view and parent view are matched.
+     * @return True if the view meets the boundsFlag, false otherwise.
+     */
+    boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) {
+        mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(),
+                mCallback.getChildStart(child), mCallback.getChildEnd(child));
+        if (boundsFlags != 0) {
+            mBoundFlags.resetFlags();
+            mBoundFlags.addFlags(boundsFlags);
+            return mBoundFlags.boundsMatch();
+        }
+        return false;
+    }
+
+    /**
+     * Callback provided by the user of this class in order to retrieve information about child and
+     * parent boundaries.
+     */
+    interface Callback {
+        int getChildCount();
+        View getParent();
+        View getChildAt(int index);
+        int getParentStart();
+        int getParentEnd();
+        int getChildStart(View view);
+        int getChildEnd(View view);
+    }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 7185cbc..5f7bf48 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -150,7 +150,11 @@
         });
     }
 
-    public View focusSearch(final View focused, final int direction)
+    public View focusSearch(final View focused, final int direction) throws Throwable {
+        return focusSearch(focused, direction, false);
+    }
+
+    public View focusSearch(final View focused, final int direction, boolean waitForScroll)
             throws Throwable {
         final View[] result = new View[1];
         mActivityRule.runOnUiThread(new Runnable() {
@@ -163,6 +167,9 @@
                 result[0] = view;
             }
         });
+        if (waitForScroll && (result[0] != null)) {
+            waitForIdleScroll(mRecyclerView);
+        }
         return result[0];
     }
 
@@ -480,7 +487,7 @@
         });
         getInstrumentation().waitForIdleSync();
         assertThat("should be able to scroll in 10 seconds", !assertArrival ||
-                viewAdded.await(10, TimeUnit.SECONDS),
+                        viewAdded.await(10, TimeUnit.SECONDS),
                 CoreMatchers.is(true));
         waitForIdleScroll(mRecyclerView);
         if (mDebug) {
@@ -548,6 +555,7 @@
             }
         }
     }
+
     public class TestLayoutManager extends RecyclerView.LayoutManager {
         int mScrollVerticallyAmount;
         int mScrollHorizontallyAmount;
@@ -1183,4 +1191,60 @@
             validateRemaining(recyclerView);
         }
     }
+
+    /**
+     * Returns whether a child of RecyclerView is partially in bound. A child is
+     * partially in-bounds if it's either fully or partially visible on the screen.
+     * @param parent The RecyclerView holding the child.
+     * @param child The child view to be checked whether is partially (or fully) within RV's bounds.
+     * @return True if the child view is partially (or fully) visible; false otherwise.
+     */
+    public static boolean isViewPartiallyInBound(RecyclerView parent, View child) {
+        if (child == null) {
+            return false;
+        }
+        final int parentLeft = parent.getPaddingLeft();
+        final int parentTop = parent.getPaddingTop();
+        final int parentRight = parent.getWidth() - parent.getPaddingRight();
+        final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+
+        final int childLeft = child.getLeft() - child.getScrollX();
+        final int childTop = child.getTop() - child.getScrollY();
+        final int childRight = child.getRight() - child.getScrollX();
+        final int childBottom = child.getBottom() - child.getScrollY();
+
+        if (childLeft >= parentRight || childRight <= parentLeft
+                || childTop >= parentBottom || childBottom <= parentTop) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns whether a child of RecyclerView is fully in-bounds, that is it's fully visible
+     * on the screen.
+     * @param parent The RecyclerView holding the child.
+     * @param child The child view to be checked whether is fully within RV's bounds.
+     * @return True if the child view is fully visible; false otherwise.
+     */
+    public boolean isViewFullyInBound(RecyclerView parent, View child) {
+        if (child == null) {
+            return false;
+        }
+        final int parentLeft = parent.getPaddingLeft();
+        final int parentTop = parent.getPaddingTop();
+        final int parentRight = parent.getWidth() - parent.getPaddingRight();
+        final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+
+        final int childLeft = child.getLeft() - child.getScrollX();
+        final int childTop = child.getTop() - child.getScrollY();
+        final int childRight = child.getRight() - child.getScrollX();
+        final int childBottom = child.getBottom() - child.getScrollY();
+
+        if (childLeft >= parentLeft && childRight <= parentRight
+                && childTop >= parentTop && childBottom <= parentBottom) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
index fff64b3..776b10d 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
@@ -14,9 +14,13 @@
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.StateListDrawable;
 import android.support.annotation.Nullable;
 import android.util.Log;
+import android.util.StateSet;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -907,7 +911,13 @@
                 lp.rightMargin = 7;
                 lp.bottomMargin = 9;
             }
-
+            // Good to have colors for debugging
+            StateListDrawable stl = new StateListDrawable();
+            stl.addState(new int[]{android.R.attr.state_focused},
+                    new ColorDrawable(Color.RED));
+            stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+            //noinspection deprecation using this for kitkat tests
+            holder.itemView.setBackgroundDrawable(stl);
             if (mOnBindCallback != null) {
                 mOnBindCallback.onBoundItem(holder, position);
             }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index db42498..4800344 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -19,6 +19,7 @@
 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
 
+import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -167,6 +168,367 @@
         }
     }
 
+
+    @Test
+    public void topUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of rows that can be fully in-bounds of RV.
+        final int visibleRowCount = 5;
+        final int spanCount = 3;
+        final int consecutiveFocusableRowsCount = 4;
+        final int consecutiveUnFocusableRowsCount = 8;
+        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
+                * spanCount;
+
+        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
+                        .reverseLayout(true),
+                new GridTestAdapter(itemCount, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation using this for kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        if (position < spanCount * consecutiveFocusableRowsCount) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
+                    }
+                });
+        waitForFirstLayout(recyclerView);
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, recyclerView.getFocusedChild());
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = focusIndex;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
+        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
+                * spanCount + visibleIndex;
+
+        // Navigate up through the focusable and unfocusable rows. The focusable rows should
+        // become focused one by one until hitting the last focusable row, at which point,
+        // unfocusable rows should become visible on the screen until the currently focused row
+        // stays on the screen.
+        int pos = focusIndex + spanCount;
+        while (pos < itemCount) {
+            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true);
+            waitForIdleScroll(recyclerView);
+            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
+            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
+            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(recyclerView, toVisible.itemView));
+            pos += spanCount;
+        }
+    }
+
+    @Test
+    public void bottomUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of rows that can be fully in-bounds of RV.
+        final int visibleRowCount = 5;
+        final int spanCount = 3;
+        final int consecutiveFocusableRowsCount = 4;
+        final int consecutiveUnFocusableRowsCount = 8;
+        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
+                * spanCount;
+
+        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
+                        .reverseLayout(false),
+                new GridTestAdapter(itemCount, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation using this for kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        if (position < spanCount * consecutiveFocusableRowsCount) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
+                    }
+                });
+        waitForFirstLayout(recyclerView);
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, recyclerView.getFocusedChild());
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = focusIndex;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
+        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
+                * spanCount + visibleIndex;
+
+        // Navigate down through the focusable and unfocusable rows. The focusable rows should
+        // become focused one by one until hitting the last focusable row, at which point,
+        // unfocusable rows should become visible on the screen until the currently focused row
+        // stays on the screen.
+        int pos = focusIndex + spanCount;
+        while (pos < itemCount) {
+            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
+            waitForIdleScroll(recyclerView);
+            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
+            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
+            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(recyclerView, toVisible.itemView));
+            pos += spanCount;
+        }
+    }
+
+    @Test
+    public void leftUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of columns that can be fully in-bounds of RV.
+        final int visibleColCount = 5;
+        final int spanCount = 3;
+        final int consecutiveFocusableColsCount = 4;
+        final int consecutiveUnFocusableColsCount = 8;
+        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
+                * spanCount;
+
+        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
+                        .orientation(HORIZONTAL).reverseLayout(true),
+                new GridTestAdapter(itemCount, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation using this for kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        if (position < spanCount * consecutiveFocusableColsCount) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
+                    }
+                });
+        waitForFirstLayout(recyclerView);
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, recyclerView.getFocusedChild());
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = focusIndex;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
+        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
+                * spanCount + visibleIndex;
+
+        // Navigate left through the focusable and unfocusable columns. The focusable columns should
+        // become focused one by one until hitting the last focusable column, at which point,
+        // unfocusable columns should become visible on the screen until the currently focused
+        // column stays on the screen.
+        int pos = focusIndex + spanCount;
+        while (pos < itemCount) {
+            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
+            waitForIdleScroll(recyclerView);
+            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
+            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
+            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(recyclerView, toVisible.itemView));
+            pos += spanCount;
+        }
+    }
+
+    @Test
+    public void rightUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of columns that can be fully in-bounds of RV.
+        final int visibleColCount = 5;
+        final int spanCount = 3;
+        final int consecutiveFocusableColsCount = 4;
+        final int consecutiveUnFocusableColsCount = 8;
+        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
+                * spanCount;
+
+        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
+                        .orientation(HORIZONTAL).reverseLayout(false),
+                new GridTestAdapter(itemCount, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation using this for kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        if (position < spanCount * consecutiveFocusableColsCount) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
+                    }
+                });
+        waitForFirstLayout(recyclerView);
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, recyclerView.getFocusedChild());
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = focusIndex;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
+        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
+                * spanCount + visibleIndex;
+
+        // Navigate right through the focusable and unfocusable columns. The focusable columns
+        // should become focused one by one until hitting the last focusable column, at which point,
+        // unfocusable columns should become visible on the screen until the currently focused
+        // column stays on the screen.
+        int pos = focusIndex + spanCount;
+        while (pos < itemCount) {
+            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
+            waitForIdleScroll(recyclerView);
+            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
+            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
+            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(recyclerView, toVisible.itemView));
+            pos += spanCount;
+        }
+    }
+
     @UiThreadTest
     @Test
     public void scrollWithoutLayout() throws Throwable {
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 5f61ea78..2da67af 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -19,16 +19,24 @@
 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
 
+import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-import android.support.test.filters.MediumTest;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.support.test.filters.LargeTest;
 import android.support.v4.view.AccessibilityDelegateCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
 import android.util.Log;
+import android.util.StateSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
@@ -37,8 +45,11 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
+
 /**
  * Includes tests for {@link LinearLayoutManager}.
  * <p>
@@ -46,10 +57,454 @@
  * and stability of LinearLayoutManager in response to different events (state change, scrolling
  * etc) where it is very hard to do manual testing.
  */
-@MediumTest
+@LargeTest
 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
 
     @Test
+    public void topUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This height ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
+                        + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(VERTICAL, false, false).adapter(adapter).reverseLayout(true),
+                false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = 0;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        // Navigate up through the focusable and unfocusable chunks. The focusable items should
+        // become focused one by one until hitting the last focusable item, at which point,
+        // unfocusable items should become visible on the screen until the currently focused item
+        // stays on the screen.
+        for (int i = 0; i < adapter.getItemCount(); i++) {
+            focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_UP, true);
+            // adapter position of the currently focused item.
+            focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
+            toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
+                    (visibleIndex + 1));
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void bottomUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This height ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
+                        + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = 0;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        // Navigate down through the focusable and unfocusable chunks. The focusable items should
+        // become focused one by one until hitting the last focusable item, at which point,
+        // unfocusable items should become visible on the screen until the currently focused item
+        // stays on the screen.
+        for (int i = 0; i < adapter.getItemCount(); i++) {
+            focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
+            // adapter position of the currently focused item.
+            focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
+            toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
+                    (visibleIndex + 1));
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void leftUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This width ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
+                        + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter).reverseLayout(true),
+                false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = 0;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        // Navigate left through the focusable and unfocusable chunks. The focusable items should
+        // become focused one by one until hitting the last focusable item, at which point,
+        // unfocusable items should become visible on the screen until the currently focused item
+        // stays on the screen.
+        for (int i = 0; i < adapter.getItemCount(); i++) {
+            focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
+            // adapter position of the currently focused item.
+            focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
+            toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
+                    (visibleIndex + 1));
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void rightUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This width ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
+                        + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter), false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        // adapter position of the item (whether focusable or not) that just becomes fully
+        // visible after focusSearch.
+        int visibleIndex = 0;
+        // The VH of the above adapter position
+        RecyclerView.ViewHolder toVisible = null;
+
+        // Navigate right through the focusable and unfocusable chunks. The focusable items should
+        // become focused one by one until hitting the last focusable item, at which point,
+        // unfocusable items should become visible on the screen until the currently focused item
+        // stays on the screen.
+        for (int i = 0; i < adapter.getItemCount(); i++) {
+            focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
+            // adapter position of the currently focused item.
+            focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
+            toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
+            visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
+                    (visibleIndex + 1));
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
+
+            assertThat("Child at position " + focusIndex + " should be focused",
+                    toFocus.itemView.hasFocus(), is(true));
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
+            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void unfocusableScrollingWhenFocusCleared() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This height ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
+                        + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
+        focusIndex++;
+        assertThat("Child at position " + focusIndex + " should be focused",
+                mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+                is(true));
+        final CountDownLatch focusLatch = new CountDownLatch(1);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){
+                    @Override
+                    public void onFocusChange(View v, boolean hasFocus) {
+                        assertNull("Focus just got cleared and no children should be holding"
+                                        + " focus now.", mRecyclerView.getFocusedChild());
+                        try {
+                            // Calling focusSearch should be a no-op here since even though there
+                            // are unfocusable views down to scroll to, none of RV's children hold
+                            // focus at this stage.
+                            View focusedChild  = focusSearch(v, View.FOCUS_DOWN, true);
+                            assertNull("Calling focusSearch should be no-op when no children hold"
+                                    + "focus", focusedChild);
+                            // No scrolling should have happened, so any unfocusables that were
+                            // invisible should still be invisible.
+                            RecyclerView.ViewHolder unforcusablePartiallyVisibleChild =
+                                    mRecyclerView.findViewHolderForAdapterPosition(
+                                            visibleChildCount - 1);
+                            assertFalse("Child view at adapter pos " + (visibleChildCount - 1)
+                                            + " should not be fully visible.",
+                                    isViewFullyInBound(mRecyclerView,
+                                            unforcusablePartiallyVisibleChild.itemView));
+                        } catch (Throwable t) {
+                            postExceptionToInstrumentation(t);
+                        }
+                    }
+                });
+                nextView.clearFocus();
+                focusLatch.countDown();
+            }
+        });
+        assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
+        assertThat("Child at position " + focusIndex + " should no longer be focused",
+                mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+                is(false));
+    }
+
+    @Test
     public void removeAnchorItem() throws Throwable {
         removeAnchorItemTest(
                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/MultiRecyclerViewPrefetchTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/MultiRecyclerViewPrefetchTest.java
index 2a615f2..aaaa43f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/MultiRecyclerViewPrefetchTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/MultiRecyclerViewPrefetchTest.java
@@ -77,6 +77,12 @@
             long getNanoTime() {
                 return mMockNanoTime;
             }
+
+            @Override
+            public int getWindowVisibility() {
+                // Pretend to be visible to avoid being filtered out
+                return View.VISIBLE;
+            }
         };
 
         // shared stats + enable clearing of pool
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 7ab616f..2ef2783 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -78,6 +78,12 @@
         long getNanoTime() {
             return mMockNanoTime;
         }
+
+        @Override
+        public int getWindowVisibility() {
+            // Pretend to be visible to avoid being filtered out
+            return View.VISIBLE;
+        }
     }
 
     @Before
@@ -737,7 +743,13 @@
         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
             mRecyclerView.registerTimePassingMs(5);
 
-            RecyclerView rv = new RecyclerView(parent.getContext());
+            RecyclerView rv = new RecyclerView(parent.getContext()) {
+                @Override
+                public int getWindowVisibility() {
+                    // Pretend to be visible to avoid being filtered out
+                    return View.VISIBLE;
+                }
+            };
             rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
                     LinearLayoutManager.HORIZONTAL, mReverseInner));
             rv.setRecycledViewPool(mSharedPool);
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 e15b9bd..cc6762a 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -51,8 +51,11 @@
 import android.os.Build;
 import android.os.SystemClock;
 import android.support.annotation.Nullable;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.Suppress;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.util.TouchUtils;
@@ -326,7 +329,7 @@
                 holder.itemView.setFocusableInTouchMode(true);
                 holder.itemView.setLayoutParams(
                         new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT));
+                                ViewGroup.LayoutParams.WRAP_CONTENT));
             }
         });
         TestLayoutManager tlm = new TestLayoutManager() {
@@ -355,7 +358,7 @@
 
             @Override
             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
-                                            RecyclerView.State state) {
+                    RecyclerView.State state) {
                 super.scrollHorizontallyBy(dx, recycler, state);
                 // offset by -dx because the views translate opposite of the scrolling direction
                 mRecyclerView.offsetChildrenHorizontal(-dx);
@@ -364,7 +367,7 @@
 
             @Override
             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
-                                          RecyclerView.State state) {
+                    RecyclerView.State state) {
                 super.scrollVerticallyBy(dy, recycler, state);
                 // offset by -dy because the views translate opposite of the scrolling direction
                 mRecyclerView.offsetChildrenVertical(-dy);
@@ -604,7 +607,7 @@
 
             @Override
             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
-                                          RecyclerView.State state) {
+                    RecyclerView.State state) {
                 // Access views in the state (that might have been deleted).
                 for (int  i = 10; i < state.getItemCount(); i++) {
                     recycler.getViewForPosition(i);
@@ -728,8 +731,8 @@
             @Nullable
             @Override
             public View onFocusSearchFailed(View focused, int direction,
-                                            RecyclerView.Recycler recycler,
-                                            RecyclerView.State state) {
+                    RecyclerView.Recycler recycler,
+                    RecyclerView.State state) {
                 try {
                     recycler.getViewForPosition(state.getItemCount() - 1);
                 } catch (Throwable t) {
@@ -1431,7 +1434,10 @@
         }
     }
 
+    @Suppress
+    @FlakyTest(bugId = 33949798)
     @Test
+    @LargeTest
     public void hasPendingUpdatesBeforeFirstLayout() throws Throwable {
         RecyclerView recyclerView = new RecyclerView(getActivity());
         TestLayoutManager layoutManager = new DumbLayoutManager();
@@ -4246,7 +4252,7 @@
 
                             @Override
                             protected void onTargetFound(View targetView, RecyclerView.State state,
-                                                         Action action) {
+                                    Action action) {
                                 super.onTargetFound(targetView, state, action);
                                 mTargetFound.set(true);
                             }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
index b9bfe40..f231c44 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
@@ -34,7 +34,10 @@
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
+import android.support.test.filters.Suppress;
 import android.support.v4.view.ViewCompat;
 import android.util.Log;
 import android.view.View;
@@ -739,7 +742,10 @@
         consistentRelayoutTest(mConfig, true);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 34158822)
     @Test
+    @LargeTest
     public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable {
         final Config config = ((Config) mConfig.clone()).itemCount(1000);
         setupByConfig(config);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
index 64111c5..c85b711 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
@@ -80,7 +80,7 @@
         smoothScrollToPosition(100);
         mLayoutManager.expectLayouts(1);
         mAdapter.deleteAndNotify(0, 2);
-        mLayoutManager.waitForLayout(2);
+        mLayoutManager.waitForLayout(2000);
         smoothScrollToPosition(0);
         assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
     }
@@ -277,9 +277,22 @@
                         holder.mBoundItem = item;
                         ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
                                 item.mText + " (" + item.mId + ")");
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation using this for kitkat tests
+                        holder.itemView.setBackgroundDrawable(stl);
+                        if (mOnBindCallback != null) {
+                            mOnBindCallback.onBoundItem(holder, position);
+                        }
                     }
                 });
-        waitFirstLayout();
+        mLayoutManager.expectLayouts(1);
+        setRecyclerView(mRecyclerView);
+        mLayoutManager.waitForLayout(10);
+        getInstrumentation().waitForIdleSync();
         ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
                 mRecyclerView.getChildCount() - 1);
         RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
@@ -391,6 +404,449 @@
         waitForIdleScroll(mRecyclerView);
     }
 
+    @Test
+    public void topUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of rows that can be fully in-bounds of RV.
+        final int visibleRowCount = 5;
+        final int spanCount = 3;
+        final int lastFocusableIndex = 6;
+
+        setupByConfig(new Config(VERTICAL, true, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
+                new GridTestAdapter(18, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        testViewHolder.itemView.setFocusable(true);
+                        testViewHolder.itemView.setFocusableInTouchMode(true);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation used to support kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+                                .getLayoutParams();
+                        if (position <= lastFocusableIndex) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
+                        lp.topMargin = 0;
+                        lp.leftMargin = 0;
+                        lp.rightMargin = 0;
+                        lp.bottomMargin = 0;
+                        if (position == 11) {
+                            lp.bottomMargin = 9;
+                        }
+                    }
+                });
+
+        /**
+         *
+         * 15 16 17
+         * 12 13 14
+         * 11 11 11
+         * 9 10
+         * 8 8 8
+         * 7
+         * 6 6 6
+         * 3 4 5
+         * 0 1 2
+         */
+        mAdapter.mFullSpanItems.add(6);
+        mAdapter.mFullSpanItems.add(8);
+        mAdapter.mFullSpanItems.add(11);
+        waitFirstLayout();
+
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
+
+        // The VH of the unfocusable item that just became fully visible after focusSearch.
+        RecyclerView.ViewHolder toVisible = null;
+
+        View focusedView = viewToFocus;
+        int actualFocusIndex = -1;
+        // First, scroll until the last focusable row.
+        for (int i : new int[]{4, 6}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+                    + actualFocusIndex, i, actualFocusIndex);
+        }
+
+        // Further scroll up in order to make the unfocusable rows visible. This process should
+        // continue until the currently focused item is still visible. The focused item should not
+        // change in this loop.
+        for (int i : new int[]{9, 11, 11, 11}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
+
+            assertEquals("Focused view should not be changed, whereas it's now at "
+                    + actualFocusIndex, 6, actualFocusIndex);
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, focusedView));
+            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void bottomUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of rows that can be fully in-bounds of RV.
+        final int visibleRowCount = 5;
+        final int spanCount = 3;
+        final int lastFocusableIndex = 6;
+
+        setupByConfig(new Config(VERTICAL, false, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
+                new GridTestAdapter(18, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        testViewHolder.itemView.setFocusable(true);
+                        testViewHolder.itemView.setFocusableInTouchMode(true);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation used to support kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+                                .getLayoutParams();
+                        if (position <= lastFocusableIndex) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
+                        lp.topMargin = 0;
+                        lp.leftMargin = 0;
+                        lp.rightMargin = 0;
+                        lp.bottomMargin = 0;
+                        if (position == 11) {
+                            lp.topMargin = 9;
+                        }
+                    }
+                });
+
+        /**
+         * 0 1 2
+         * 3 4 5
+         * 6 6 6
+         * 7
+         * 8 8 8
+         * 9 10
+         * 11 11 11
+         * 12 13 14
+         * 15 16 17
+         */
+        mAdapter.mFullSpanItems.add(6);
+        mAdapter.mFullSpanItems.add(8);
+        mAdapter.mFullSpanItems.add(11);
+        waitFirstLayout();
+
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
+
+        // The VH of the unfocusable item that just became fully visible after focusSearch.
+        RecyclerView.ViewHolder toVisible = null;
+
+        View focusedView = viewToFocus;
+        int actualFocusIndex = -1;
+        // First, scroll until the last focusable row.
+        for (int i : new int[]{4, 6}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+                    + actualFocusIndex, i, actualFocusIndex);
+        }
+
+        // Further scroll down in order to make the unfocusable rows visible. This process should
+        // continue until the currently focused item is still visible. The focused item should not
+        // change in this loop.
+        for (int i : new int[]{9, 11, 11, 11}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
+
+            assertEquals("Focused view should not be changed, whereas it's now at "
+                    + actualFocusIndex, 6, actualFocusIndex);
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, focusedView));
+            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void leftUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of columns that can be fully in-bounds of RV.
+        final int visibleColCount = 5;
+        final int spanCount = 3;
+        final int lastFocusableIndex = 6;
+
+        // Reverse layout so that views are placed from right to left.
+        setupByConfig(new Config(HORIZONTAL, true, spanCount,
+                        GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
+                new GridTestAdapter(18, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        testViewHolder.itemView.setFocusable(true);
+                        testViewHolder.itemView.setFocusableInTouchMode(true);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation used to support kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+                                .getLayoutParams();
+                        if (position <= lastFocusableIndex) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
+                        lp.topMargin = 0;
+                        lp.leftMargin = 0;
+                        lp.rightMargin = 0;
+                        lp.bottomMargin = 0;
+                        if (position == 11) {
+                            lp.rightMargin = 9;
+                        }
+                    }
+                });
+
+        /**
+         * 15 12 11 9  8 7 6 3 0
+         * 16 13 11 10 8   6 4 1
+         * 17 14 11    8   6 5 2
+         */
+        mAdapter.mFullSpanItems.add(6);
+        mAdapter.mFullSpanItems.add(8);
+        mAdapter.mFullSpanItems.add(11);
+        waitFirstLayout();
+
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
+
+        // The VH of the unfocusable item that just became fully visible after focusSearch.
+        RecyclerView.ViewHolder toVisible = null;
+
+        View focusedView = viewToFocus;
+        int actualFocusIndex = -1;
+        // First, scroll until the last focusable column.
+        for (int i : new int[]{4, 6}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+                    + actualFocusIndex, i, actualFocusIndex);
+        }
+
+        // Further scroll left in order to make the unfocusable columns visible. This process should
+        // continue until the currently focused item is still visible. The focused item should not
+        // change in this loop.
+        for (int i : new int[]{9, 11, 11, 11}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
+
+            assertEquals("Focused view should not be changed, whereas it's now at "
+                    + actualFocusIndex, 6, actualFocusIndex);
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, focusedView));
+            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
+
+    @Test
+    public void rightUnfocusableViewsVisibility() throws Throwable {
+        // The maximum number of columns that can be fully in-bounds of RV.
+        final int visibleColCount = 5;
+        final int spanCount = 3;
+        final int lastFocusableIndex = 6;
+
+        setupByConfig(new Config(HORIZONTAL, false, spanCount,
+                        GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
+                new GridTestAdapter(18, 1) {
+                    RecyclerView mAttachedRv;
+
+                    @Override
+                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                            int viewType) {
+                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                        testViewHolder.itemView.setFocusable(true);
+                        testViewHolder.itemView.setFocusableInTouchMode(true);
+                        // Good to have colors for debugging
+                        StateListDrawable stl = new StateListDrawable();
+                        stl.addState(new int[]{android.R.attr.state_focused},
+                                new ColorDrawable(Color.RED));
+                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                        //noinspection deprecation used to support kitkat tests
+                        testViewHolder.itemView.setBackgroundDrawable(stl);
+                        return testViewHolder;
+                    }
+
+                    @Override
+                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                        mAttachedRv = recyclerView;
+                    }
+
+                    @Override
+                    public void onBindViewHolder(TestViewHolder holder,
+                            int position) {
+                        super.onBindViewHolder(holder, position);
+                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+                                .getLayoutParams();
+                        if (position <= lastFocusableIndex) {
+                            holder.itemView.setFocusable(true);
+                            holder.itemView.setFocusableInTouchMode(true);
+                        } else {
+                            holder.itemView.setFocusable(false);
+                            holder.itemView.setFocusableInTouchMode(false);
+                        }
+                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
+                        lp.topMargin = 0;
+                        lp.leftMargin = 0;
+                        lp.rightMargin = 0;
+                        lp.bottomMargin = 0;
+                        if (position == 11) {
+                            lp.leftMargin = 9;
+                        }
+                    }
+                });
+
+        /**
+         * 0 3 6 7 8 9  11 12 15
+         * 1 4 6   8 10 11 13 16
+         * 2 5 6   8    11 14 17
+         */
+        mAdapter.mFullSpanItems.add(6);
+        mAdapter.mFullSpanItems.add(8);
+        mAdapter.mFullSpanItems.add(11);
+        waitFirstLayout();
+
+
+        // adapter position of the currently focused item.
+        int focusIndex = 1;
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        View viewToFocus = toFocus.itemView;
+        assertTrue(requestFocus(viewToFocus, true));
+        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
+
+        // The VH of the unfocusable item that just became fully visible after focusSearch.
+        RecyclerView.ViewHolder toVisible = null;
+
+        View focusedView = viewToFocus;
+        int actualFocusIndex = -1;
+        // First, scroll until the last focusable column.
+        for (int i : new int[]{4, 6}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+                    + actualFocusIndex, i, actualFocusIndex);
+        }
+
+        // Further scroll right in order to make the unfocusable rows visible. This process should
+        // continue until the currently focused item is still visible. The focused item should not
+        // change in this loop.
+        for (int i : new int[]{9, 11, 11, 11}) {
+            focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
+            focusedView = mRecyclerView.getFocusedChild();
+            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
+            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
+
+            assertEquals("Focused view should not be changed, whereas it's now at "
+                    + actualFocusIndex, 6, actualFocusIndex);
+            assertTrue("Focused child should be at least partially visible.",
+                    isViewPartiallyInBound(mRecyclerView, focusedView));
+            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
+                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
+        }
+    }
 
     @Test
     public void scrollToPositionWithPredictive() throws Throwable {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ViewBoundsCheckTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/ViewBoundsCheckTest.java
new file mode 100644
index 0000000..3d2affb
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/ViewBoundsCheckTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class ViewBoundsCheckTest {
+
+
+    private static final String TAG = "ViewBoundsCheckTest";
+    private Context mContext;
+
+    /** Case #1:
+     * Parent:                    [2.......................8]
+     Views: [-3...-1] [-1...1] [1...3] [3...5] [5...7] [7...9] [9...11] [11...13]
+     */
+    int[] mParentBound1 = {2, 8};
+    int[][] mChildrenBound1 = {{-3, -1}, {-1, 1}, {1, 3}, {3, 5}, {5, 7}, {7, 9}, {9, 11},
+            {11, 13}};
+
+    /** Case #2:
+     * Parent:                  [1...................7]
+     Views: [-3...-1] [-1...1][1...3] [3...5] [5...7] [7...9] [9...11]
+     */
+    int[] mParentBound2 = {1, 7};
+    int[][] mChildrenBound2 = {{-3, -1}, {-1, 1}, {1, 3}, {3, 5}, {5, 7}, {7, 9}, {9, 11}};
+
+    View mParent;
+    View[] mChildren;
+
+    private final ViewBoundsCheck.Callback mBoundCheckCallback =
+            new ViewBoundsCheck.Callback() {
+                @Override
+                public int getChildCount() {
+                    return mChildren.length;
+                }
+
+                @Override
+                public View getParent() {
+                    return mParent;
+                }
+
+                @Override
+                public View getChildAt(int index) {
+                    return mChildren[index];
+                }
+
+                @Override
+                public int getParentStart() {
+                    return mParent.getLeft();
+                }
+
+                @Override
+                public int getParentEnd() {
+                    return mParent.getRight();
+                }
+
+                @Override
+                public int getChildStart(View view) {
+                    return view.getLeft();
+                }
+
+                @Override
+                public int getChildEnd(View view) {
+                    return view.getRight();
+                }
+            };
+
+    ViewBoundsCheck mBoundCheck = new ViewBoundsCheck(mBoundCheckCallback);
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    private void setUpViews(int[] parentBound, int[][] childrenBound) {
+        mParent = new View(mContext);
+        mParent.setLeft(parentBound[0]);
+        mParent.setRight(parentBound[1]);
+        mChildren = new View[childrenBound.length];
+        for (int i = 0; i < childrenBound.length; i++) {
+            mChildren[i] = new View(mContext);
+            mChildren[i].setLeft(childrenBound[i][0]);
+            mChildren[i].setRight(childrenBound[i][1]);
+        }
+    }
+
+    @Test
+    public void firstFullyVisibleChildFromStart() {
+        setUpViews(mParentBound1, mChildrenBound1);
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = ViewBoundsCheck.FLAG_CVS_GT_PVS
+                | ViewBoundsCheck.FLAG_CVS_EQ_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_EQ_PVE;
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0;
+        View view = mBoundCheck.findOneViewWithinBoundFlags(0, mChildren.length,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first fully visible child from start should be returned", 3,
+                view.getLeft());
+        assertEquals("The first fully visible child from start should be returned", 5,
+                view.getRight());
+    }
+
+    @Test
+    public void firstFullyVisibleChildFromEnd() {
+        setUpViews(mParentBound1, mChildrenBound1);
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = ViewBoundsCheck.FLAG_CVS_GT_PVS
+                | ViewBoundsCheck.FLAG_CVS_EQ_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_EQ_PVE;
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0;
+        View view = mBoundCheck.findOneViewWithinBoundFlags(mChildren.length - 1, -1,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first fully visible child from end should be returned", 5,
+                view.getLeft());
+        assertEquals("The first fully visible child from end should be returned", 7,
+                view.getRight());
+    }
+
+    @Test
+    public void firstPartiallyOrFullyVisibleChildFromStartWithViewBoundsNotAligned() {
+        setUpViews(mParentBound1, mChildrenBound1);
+        // These set of flags are used in LinearLayoutManager#findOneVisibleChild
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(0, mChildren.length,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first partially visible child from start should be returned", 1,
+                view.getLeft());
+        assertEquals("The first partially visible child from start should be returned", 3,
+                view.getRight());
+    }
+
+    @Test
+    public void firstPartiallyOrFullyVisibleChildFromStartWithViewBoundsAligned() {
+        setUpViews(mParentBound2, mChildrenBound2);
+        // These set of flags are used in LinearLayoutManager#findOneVisibleChild
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(0, mChildren.length,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first partially visible child from start should be returned", 1,
+                view.getLeft());
+        assertEquals("The first partially visible child from start should be returned", 3,
+                view.getRight());
+    }
+
+    @Test
+    public void firstPartiallyOrFullyVisibleChildFromEndWithViewBoundsNotAligned() {
+        setUpViews(mParentBound1, mChildrenBound1);
+        // These set of flags are used in LinearLayoutManager#findOneVisibleChild
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(mChildren.length - 1, -1,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first partially visible child from end should be returned", 7,
+                view.getLeft());
+        assertEquals("The first partially visible child from end should be returned", 9,
+                view.getRight());
+    }
+
+    @Test
+    public void firstPartiallyOrFullyVisibleChildFromEndWithViewBoundsAligned() {
+        setUpViews(mParentBound2, mChildrenBound2);
+        // These set of flags are used in LinearLayoutManager#findOneVisibleChild
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE
+                | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(mChildren.length - 1, -1,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The first partially visible child from end should be returned", 5,
+                view.getLeft());
+        assertEquals("The first partially visible child from end should be returned", 7,
+                view.getRight());
+    }
+
+    @Test
+    public void lastFullyInvisibleChildFromStart() {
+        setUpViews(mParentBound2, mChildrenBound2);
+        @ViewBoundsCheck.ViewBounds int  preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS
+                | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_GT_PVS);
+        @ViewBoundsCheck.ViewBounds int  acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS
+                | ViewBoundsCheck.FLAG_CVE_LT_PVE);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(0, mChildren.length,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The last fully invisible child from start should be returned", -1,
+                view.getLeft());
+        assertEquals("TThe last fully invisible child from start should be returned", 1,
+                view.getRight());
+    }
+
+    @Test
+    public void lastFullyInvisibleChildFromEnd() {
+        setUpViews(mParentBound2, mChildrenBound2);
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE
+                | ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_LT_PVE);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE
+                | ViewBoundsCheck.FLAG_CVS_GT_PVS);
+        View view = mBoundCheck.findOneViewWithinBoundFlags(mChildren.length - 1, -1,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertEquals("The last fully invisible child from end should be returned", 7,
+                view.getLeft());
+        assertEquals("TThe last fully invisible child from end should be returned", 9,
+                view.getRight());
+    }
+
+    @Test
+    public void noViewsFoundWithinGivenBounds() {
+        setUpViews(mParentBound1, mChildrenBound1);
+        // create a view whose bounds cover its parent. Since no such view exist in the example
+        // layout, null should be returned.
+        @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS
+                | ViewBoundsCheck.FLAG_CVE_GT_PVE);
+        @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = preferredBoundsFlag;
+        View view = mBoundCheck.findOneViewWithinBoundFlags(0, mChildren.length,
+                preferredBoundsFlag, acceptableBoundsFlag);
+        assertNull("Null should be returned since no views are within the given bounds",
+                view);
+    }
+
+}