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