diff --git a/build.gradle b/build.gradle index 14fc8ff15c0..70b4cd10290 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0-beta3' + classpath 'com.android.tools.build:gradle:2.1.0-rc1' } } diff --git a/mobile/com.sap.sailing.android.shared/.classpath b/mobile/com.sap.sailing.android.shared/.classpath index a0a969274af..e89fb998e31 100755 --- a/mobile/com.sap.sailing.android.shared/.classpath +++ b/mobile/com.sap.sailing.android.shared/.classpath @@ -1,16 +1,17 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/mobile/com.sap.sailing.android.shared/project.properties b/mobile/com.sap.sailing.android.shared/project.properties index fac622301eb..708e26d206c 100755 --- a/mobile/com.sap.sailing.android.shared/project.properties +++ b/mobile/com.sap.sailing.android.shared/project.properties @@ -16,3 +16,5 @@ android.library=true android.library.reference.1=../google-android-support-v7-appcompat_lib android.library.reference.2=../licensesdialog_lib/de.psdev.licensesdialog +android.library.reference.3=../google-android-support-v4 + diff --git a/mobile/com.sap.sailing.racecommittee.app/.classpath b/mobile/com.sap.sailing.racecommittee.app/.classpath index 886ac925c10..0e842263cf7 100644 --- a/mobile/com.sap.sailing.racecommittee.app/.classpath +++ b/mobile/com.sap.sailing.racecommittee.app/.classpath @@ -1,13 +1,14 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/mobile/com.sap.sailing.racecommittee.app/build.gradle b/mobile/com.sap.sailing.racecommittee.app/build.gradle index ff444e72e88..78e5e863cb1 100644 --- a/mobile/com.sap.sailing.racecommittee.app/build.gradle +++ b/mobile/com.sap.sailing.racecommittee.app/build.gradle @@ -72,7 +72,7 @@ android { } dependencies { - compile "com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.6.2" + compile "com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.7.4" /* support libraries */ compile "com.android.support:appcompat-v7:${rootProject.ext.support}" diff --git a/mobile/com.sap.sailing.racecommittee.app/pom.xml b/mobile/com.sap.sailing.racecommittee.app/pom.xml index 32064511831..e5c1627ea2f 100644 --- a/mobile/com.sap.sailing.racecommittee.app/pom.xml +++ b/mobile/com.sap.sailing.racecommittee.app/pom.xml @@ -35,7 +35,7 @@ com.h6ah4i.android.widget.advrecyclerview advrecyclerview - 0.6.2 + 0.7.4 aar diff --git a/mobile/com.sap.sailing.racecommittee.app/project.properties b/mobile/com.sap.sailing.racecommittee.app/project.properties index 5748d82c066..c16fb912f8d 100755 --- a/mobile/com.sap.sailing.racecommittee.app/project.properties +++ b/mobile/com.sap.sailing.racecommittee.app/project.properties @@ -12,8 +12,9 @@ # Project target. target=Google Inc.:Google APIs:22 -android.library.reference.1=../google-play-services_lib -android.library.reference.2=../com.sap.sailing.android.shared -android.library.reference.3=../google-android-support-v7-recyclerview_lib -android.library.reference.4=../github-advrecyclerview_lib -android.library.reference.5=../licensesdialog_lib/de.psdev.licensesdialog +android.library.reference.1=..\\google-play-services_lib +android.library.reference.2=..\\com.sap.sailing.android.shared +android.library.reference.3=..\\google-android-support-v7-recyclerview_lib +android.library.reference.4=..\\github-advrecyclerview_lib +android.library.reference.5=..\\licensesdialog_lib\\de.psdev.licensesdialog +android.library.reference.6=../google-android-support-v13 diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java deleted file mode 100644 index b9ed2bb06cd..00000000000 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.sap.sailing.racecommittee.app.ui.adapters; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder; - -public abstract class BaseDraggableSwipeViewHolder extends RecyclerView.ViewHolder - implements DraggableItemViewHolder, SwipeableItemViewHolder { - - private int mDragStateFlags; - private int mSwipeStateFlags; - private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE; - private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; - private float mSwipeAmount; - - public BaseDraggableSwipeViewHolder(View itemView) { - super(itemView); - } - - @Override - public void setDragStateFlags(int flags) { - mDragStateFlags = flags; - } - - @Override - public int getDragStateFlags() { - return mDragStateFlags; - } - - @Override - public void setSwipeStateFlags(int flags) { - mSwipeStateFlags = flags; - } - - @Override - public int getSwipeStateFlags() { - return mSwipeStateFlags; - } - - @Override - public void setSwipeResult(int result) { - mSwipeResult = result; - } - - @Override - public int getSwipeResult() { - return mSwipeResult; - } - - @Override - public void setAfterSwipeReaction(int reaction) { - mAfterSwipeReaction = reaction; - } - - @Override - public int getAfterSwipeReaction() { - return mAfterSwipeReaction; - } - - @Override - public void setSwipeItemSlideAmount(float amount) { - mSwipeAmount = amount; - } - - @Override - public float getSwipeItemSlideAmount() { - return mSwipeAmount; - } -} diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java index 4793a87b657..09625c7da3b 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java @@ -2,6 +2,17 @@ package com.sap.sailing.racecommittee.app.ui.adapters; import java.util.List; +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; +import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; +import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder; +import com.sap.sailing.android.shared.util.BitmapHelper; +import com.sap.sailing.android.shared.util.ViewHelper; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.racecommittee.app.R; +import com.sap.sailing.racecommittee.app.domain.impl.CompetitorResultWithIdImpl; +import com.sap.sailing.racecommittee.app.utils.ThemeHelper; + import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v4.view.ViewCompat; @@ -10,16 +21,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; -import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.sap.sailing.android.shared.util.BitmapHelper; -import com.sap.sailing.android.shared.util.ViewHelper; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.racecommittee.app.R; -import com.sap.sailing.racecommittee.app.domain.impl.CompetitorResultWithIdImpl; -import com.sap.sailing.racecommittee.app.utils.ThemeHelper; - public class FinishListAdapter extends BaseDraggableSwipeAdapter { private Context mContext; @@ -80,7 +81,7 @@ public class FinishListAdapter extends BaseDraggableSwipeAdapter { private final static String TAG = CourseElementAdapter.class.getName(); @@ -195,7 +195,7 @@ public class CourseElementAdapter extends BaseDraggableSwipeAdapter - - - - - - - - + + + + + + + + + + + + diff --git a/mobile/github-advrecyclerview_lib/AndroidManifest.xml b/mobile/github-advrecyclerview_lib/AndroidManifest.xml index 80ca8cddac4..7e6be5e490b 100644 --- a/mobile/github-advrecyclerview_lib/AndroidManifest.xml +++ b/mobile/github-advrecyclerview_lib/AndroidManifest.xml @@ -1,3 +1,4 @@ + + package="com.h6ah4i.android.widget.advrecyclerview" + android:versionCode="9" + android:versionName="0.7.4" > - - - - - + + + \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/README b/mobile/github-advrecyclerview_lib/README new file mode 100755 index 00000000000..2d375229d0a --- /dev/null +++ b/mobile/github-advrecyclerview_lib/README @@ -0,0 +1,2 @@ +advrecyclerview-0.7.4 ist the last version that depends on Android 22.2.1 which is what we're currently using. +See http://jcenter.bintray.com/com/h6ah4i/android/widget/advrecyclerview/advrecyclerview/0.7.4/. \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/project.properties b/mobile/github-advrecyclerview_lib/project.properties index eba516190a6..362a0a3082a 100644 --- a/mobile/github-advrecyclerview_lib/project.properties +++ b/mobile/github-advrecyclerview_lib/project.properties @@ -12,6 +12,4 @@ # Project target. target=android-22 -android.library.reference.1=../google-android-support-v7-recyclerview_lib android.library=true -android.library.reference.2=../google-android-support-v7-appcompat_lib diff --git a/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF b/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java old mode 100644 new mode 100755 index f6f3aad8e2e..8d25e41ef1e --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java @@ -36,7 +36,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { private ItemChangeAnimationManager mChangeAnimationsManager; private ItemMoveAnimationManager mMoveAnimationsManager; - GeneralItemAnimator() { + protected GeneralItemAnimator() { setup(); } @@ -65,7 +65,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateRemove(final RecyclerView.ViewHolder holder) { if (mDebug) { - Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); + Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ")"); } return mRemoveAnimationManager.addPendingAnimation(holder); @@ -74,7 +74,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { if (mDebug) { - Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); + Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ")"); } return mAddAnimationsManager.addPendingAnimation(holder); @@ -83,7 +83,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { if (mDebug) { - Log.d(TAG, "animateMove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); + Log.d(TAG, "animateMove(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); } return mMoveAnimationsManager.addPendingAnimation(holder, fromX, fromY, toX, toY); @@ -94,9 +94,9 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { if (mDebug) { final String oldId = (oldHolder != null) ? Long.toString(oldHolder.getItemId()) : "-"; - final String oldPosition = (oldHolder != null) ? Long.toString(oldHolder.getPosition()) : "-"; + final String oldPosition = (oldHolder != null) ? Long.toString(oldHolder.getLayoutPosition()) : "-"; final String newId = (newHolder != null) ? Long.toString(newHolder.getItemId()) : "-"; - final String newPosition = (newHolder != null) ? Long.toString(newHolder.getPosition()) : "-"; + final String newPosition = (newHolder != null) ? Long.toString(newHolder.getLayoutPosition()) : "-"; Log.d(TAG, "animateChange(old.id = " + oldId + ", old.position = " + oldPosition + ", new.id = " + newId + ", new.position = " + newPosition + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java old mode 100644 new mode 100755 index fe6d8dbe6ca..ff74aa686a9 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java @@ -48,7 +48,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { + protected static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); @@ -93,7 +93,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager { + protected static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager { public DefaultItemRemoveAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); @@ -137,7 +137,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager { + protected static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager { public DefaultItemChangeAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); } @@ -220,7 +220,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager { + protected static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager { public DefaultItemMoveAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java old mode 100644 new mode 100755 index d638bf6f3f1..0c7e8fb4303 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java @@ -78,8 +78,6 @@ public class ItemShadowDecorator extends RecyclerView.ItemDecoration { } c.restoreToCount(savedCount); - - ViewCompat.postInvalidateOnAnimation(parent); } private static boolean shouldDrawDropShadow(View child) { diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java old mode 100644 new mode 100755 index bf257edb9ca..89aa001f27c --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java @@ -24,22 +24,24 @@ public interface DraggableItemAdapter { * Called when user is attempt to drag the item. * * @param holder The ViewHolder which is associated to item user is attempt to start dragging. + * @param position The position of the item within the adapter's data set. * @param x Touched X position. Relative from the itemView's top-left. * @param y Touched Y position. Relative from the itemView's top-left. * @return Whether can start dragging. */ - boolean onCheckCanStartDrag(T holder, int x, int y); + boolean onCheckCanStartDrag(T holder, int position, int x, int y); /** - * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method returned true. + * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method returned true. * * @param holder The ViewHolder which is associated to item user is attempt to start dragging. + * @param position The position of the item within the adapter's data set. * * @return null: no constraints (= new ItemDraggableRange(0, getItemCount() - 1)), * otherwise: the range specified item can be drag-sortable. */ - ItemDraggableRange onGetItemDraggableRange(T holder); + ItemDraggableRange onGetItemDraggableRange(T holder, int position); /** * Called when item is moved. Should apply the move operation result to data set. diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java old mode 100644 new mode 100755 index f74d9515520..745e6cad1b3 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java @@ -20,16 +20,19 @@ import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.ViewGroup; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter; import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; -class DraggableItemWrapperAdapter extends BaseWrapperAdapter { +class DraggableItemWrapperAdapter extends BaseWrapperAdapter implements SwipeableItemAdapter { private static final String TAG = "ARVDraggableWrapper"; private static final int STATE_FLAG_INITIAL_VALUE = -1; private static final boolean LOCAL_LOGV = false; private static final boolean LOCAL_LOGD = false; + private static final boolean LOCAL_LOGI = true; private static final boolean DEBUG_BYPASS_MOVE_OPERATION_MODE = false; private RecyclerViewDragDropManager mDragDropManager; @@ -83,7 +86,15 @@ class DraggableItemWrapperAdapter extends Ba position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); if (itemId == mDraggingItemId && holder != mDraggingItem) { - Log.w(TAG, "an another view holder object for the currently dragging item is assigned"); + if (mDraggingItem == null) { + if (LOCAL_LOGI) { + Log.i(TAG, "a new view holder object for the currently dragging item is assigned"); + } + mDraggingItem = holder; + mDragDropManager.onNewDraggingItemViewBinded(holder); + } else { + Log.e(TAG, "an another view holder object for the currently dragging item is assigned"); + } } int flags = RecyclerViewDragDropManager.STATE_FLAG_DRAGGING; @@ -223,7 +234,7 @@ class DraggableItemWrapperAdapter extends Ba throw new IllegalStateException("dragging target must provides valid ID"); } - mDraggingItemInitialPosition = holder.getPosition(); + mDraggingItemInitialPosition = holder.getAdapterPosition(); mDraggingItemCurrentPosition = mDraggingItemInitialPosition; mDraggingItem = holder; mDraggingItemId = holder.getItemId(); @@ -238,7 +249,7 @@ class DraggableItemWrapperAdapter extends Ba Log.d(TAG, "onDragItemFinished(holder = " + holder + ", result = " + result + ")"); } - if (holder != mDraggingItem) { + if (holder != null && holder != mDraggingItem) { throw new IllegalStateException("onDragItemFinished() - may be a bug (mDraggingItem != holder)"); } @@ -248,8 +259,9 @@ class DraggableItemWrapperAdapter extends Ba if (result && (mDraggingItemCurrentPosition != mDraggingItemInitialPosition)) { // apply to wrapped adapter - ((DraggableItemAdapter) getWrappedAdapter()).onMoveItem( - mDraggingItemInitialPosition, mDraggingItemCurrentPosition); + DraggableItemAdapter adapter = WrapperAdapterUtils.findWrappedAdapter( + getWrappedAdapter(), DraggableItemAdapter.class); + adapter.onMoveItem(mDraggingItemInitialPosition, mDraggingItemCurrentPosition); } mDraggingItemInitialPosition = RecyclerView.NO_POSITION; @@ -265,13 +277,11 @@ class DraggableItemWrapperAdapter extends Ba public void onViewRecycled(VH holder) { if (isDragging()) { if (holder == mDraggingItem) { - try { - throw new IllegalStateException("onViewRecycled() - may be a bug. dragging item should be recycled."); - } catch (IllegalStateException e) { - // TODO is there any way to prevent the dragging item from be recycled...? - - mDragDropManager.cancelDrag(); + if (LOCAL_LOGI) { + Log.i(TAG, "a view holder object which is bound to currently dragging item is recycled"); } + mDraggingItem = null; + mDragDropManager.onDraggingItemViewRecycled(); } } @@ -281,21 +291,21 @@ class DraggableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - boolean canStartDrag(RecyclerView.ViewHolder holder, int x, int y) { + boolean canStartDrag(RecyclerView.ViewHolder holder, int position, int x, int y) { if (LOCAL_LOGV) { - Log.v(TAG, "canStartDrag(holder = " + holder + ", x = " + x + ", y = " + y + ")"); + Log.v(TAG, "canStartDrag(holder = " + holder + ", position = " + position + ", x = " + x + ", y = " + y + ")"); } - return mDraggableItemAdapter.onCheckCanStartDrag(holder, x, y); + return mDraggableItemAdapter.onCheckCanStartDrag(holder, position, x, y); } // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder) { + ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder, int position) { if (LOCAL_LOGV) { - Log.v(TAG, "getItemDraggableRange(holder = " + holder + ")"); + Log.v(TAG, "getItemDraggableRange(holder = " + holder + ", position = " + position + ")"); } - return mDraggableItemAdapter.onGetItemDraggableRange(holder); + return mDraggableItemAdapter.onGetItemDraggableRange(holder, position); } // NOTE: This method is called from RecyclerViewDragDropManager @@ -335,6 +345,14 @@ class DraggableItemWrapperAdapter extends Ba return mDragDropManager.isDragging(); } + /*package*/ int getDraggingItemInitialPosition() { + return mDraggingItemInitialPosition; + } + + /*package*/ int getDraggingItemCurrentPosition() { + return mDraggingItemCurrentPosition; + } + private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) { if (!(holder instanceof DraggableItemViewHolder)) { return; @@ -356,4 +374,75 @@ class DraggableItemWrapperAdapter extends Ba private static DraggableItemAdapter getDraggableItemAdapter(RecyclerView.Adapter adapter) { return WrapperAdapterUtils.findWrappedAdapter(adapter, DraggableItemAdapter.class); } + + private int getOriginalPosition(int position) { + int correctedPosition; + + if (isDragging()) { + correctedPosition = convertToOriginalPosition( + position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); + } else { + correctedPosition = position; + } + return correctedPosition; + } + + // + // SwipeableItemAdapter implementations + // + @SuppressWarnings("unchecked") + @Override + public int onGetSwipeReactionType(VH holder, int position, int x, int y) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + return swipeableItemAdapter.onGetSwipeReactionType(holder, correctedPosition, x, y); + } + + @SuppressWarnings("unchecked") + @Override + public void onSetSwipeBackground(VH holder, int position, int type) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + swipeableItemAdapter.onSetSwipeBackground(holder, correctedPosition, type); + } + + @SuppressWarnings("unchecked") + @Override + public int onSwipeItem(VH holder, int position, int result) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + return swipeableItemAdapter.onSwipeItem(holder, correctedPosition, result); + } + + @SuppressWarnings("unchecked") + @Override + public void onPerformAfterSwipeReaction(VH holder, int position, int result, int reaction) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + swipeableItemAdapter.onPerformAfterSwipeReaction(holder, correctedPosition, result, reaction); + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java old mode 100644 new mode 100755 index 00e6bb4d1cd..463fd97b1ad --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java @@ -104,10 +104,14 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { // return to default position updateDraggingItemPosition(mTranslationY); - moveToDefaultPosition(mDraggingItem.itemView, animate); + if (mDraggingItem != null) { + moveToDefaultPosition(mDraggingItem.itemView, animate); + } // show - mDraggingItem.itemView.setVisibility(View.VISIBLE); + if (mDraggingItem != null) { + mDraggingItem.itemView.setVisibility(View.VISIBLE); + } mDraggingItem = null; if (mDraggingItemImage != null) { @@ -178,6 +182,13 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { mTranslationY = Math.min(Math.max(mTranslationY, mTranslationTopLimit), mTranslationBottomLimit); } + public boolean isReachedToTopLimit() { + return (mTranslationY == mTranslationTopLimit); + } + + public boolean isReachedToBottomLimit() { + return (mTranslationY == mTranslationBottomLimit); + } private Bitmap createDraggingItemImage(View v, NinePatchDrawable shadow) { int width = v.getWidth() + mShadowPadding.left + mShadowPadding.right; @@ -204,7 +215,9 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { private void updateDraggingItemPosition(int translationY) { // NOTE: Need to update the view position to make other decorations work properly while dragging - setItemTranslationY(mRecyclerView, mDraggingItem, translationY - mDraggingItem.itemView.getTop()); + if (mDraggingItem != null) { + setItemTranslationY(mRecyclerView, mDraggingItem, translationY - mDraggingItem.itemView.getTop()); + } } public void setIsScrolling(boolean isScrolling) { @@ -238,7 +251,7 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); if (vh != null) { - final int position = vh.getPosition(); + final int position = vh.getLayoutPosition(); if ((position >= firstVisiblePosition) && (position <= lastVisiblePosition) && @@ -266,7 +279,7 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); if (vh != null) { - final int position = vh.getPosition(); + final int position = vh.getLayoutPosition(); if ((position >= firstVisiblePosition) && (position <= lastVisiblePosition) && @@ -279,4 +292,22 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { return v; } + + public void invalidateDraggingItem() { + if (mDraggingItem != null) { + mDraggingItem.itemView.setVisibility(View.VISIBLE); + } + + mDraggingItem = null; + } + + public void setDraggingItemViewHolder(RecyclerView.ViewHolder holder) { + if (mDraggingItem != null) { + throw new IllegalStateException("A new view holder is attempt to be assigned before invalidating the older one"); + } + + mDraggingItem = holder; + + holder.itemView.setVisibility(View.INVISIBLE); + } } \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java old mode 100644 new mode 100755 index 372a595aec8..4a9454c7590 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java @@ -82,16 +82,15 @@ public class EdgeEffectDecorator extends RecyclerView.ItemDecoration { public void pullTopGlow(float deltaDistance) { ensureTopGlow(mRecyclerView); - if (mTopGlow.onPull(deltaDistance)) { + if (mTopGlow.onPull(deltaDistance, 0.5f)) { ViewCompat.postInvalidateOnAnimation(mRecyclerView); } } public void pullBottom(float deltaDistance) { ensureBottomGlow(mRecyclerView); - mBottomGlow.onPull(deltaDistance); - if (mBottomGlow.onPull(deltaDistance)) { + if (mBottomGlow.onPull(deltaDistance, 0.5f)) { ViewCompat.postInvalidateOnAnimation(mRecyclerView); } } @@ -114,31 +113,33 @@ public class EdgeEffectDecorator extends RecyclerView.ItemDecoration { } private void ensureTopGlow(RecyclerView rv) { - if (mTopGlow != null) { - return; - } - mTopGlow = new EdgeEffectCompat(rv.getContext()); - if (getClipToPadding(rv)) { - mTopGlow.setSize( - rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(), - rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom()); - } else { - mTopGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight()); + if (mTopGlow == null) { + mTopGlow = new EdgeEffectCompat(rv.getContext()); } + + updateGlowSize(rv, mTopGlow); } private void ensureBottomGlow(RecyclerView rv) { - if (mBottomGlow != null) { - return; + if (mBottomGlow == null) { + mBottomGlow = new EdgeEffectCompat(rv.getContext()); } - mBottomGlow = new EdgeEffectCompat(rv.getContext()); + updateGlowSize(rv, mBottomGlow); + } + + private static void updateGlowSize(RecyclerView rv, EdgeEffectCompat topGlow) { + int width = rv.getMeasuredWidth(); + int height = rv.getMeasuredHeight(); + if (getClipToPadding(rv)) { - mBottomGlow.setSize( - rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(), - rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom()); - } else { - mBottomGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight()); + width -= rv.getPaddingLeft() - rv.getPaddingRight(); + height -= rv.getPaddingTop() - rv.getPaddingBottom(); } + + width = Math.max(0, width); + height = Math.max(0, height); + + topGlow.setSize(width, height); } private static boolean getClipToPadding(RecyclerView rv) { diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java old mode 100644 new mode 100755 index 2625cf5120d..e5bbde8baf6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java @@ -41,9 +41,13 @@ public class ItemDraggableRange { return ((position >= mStart) && (position <= mEnd)); } + protected String getClassName() { + return "ItemDraggableRange"; + } + @Override public String toString() { - return "ItemDraggableRange{" + + return getClassName() + "{" + "mStart=" + mStart + ", mEnd=" + mEnd + '}'; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java old mode 100644 new mode 100755 index 41667008824..9cdef404af8 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java @@ -19,6 +19,8 @@ package com.h6ah4i.android.widget.advrecyclerview.draggable; import android.graphics.Rect; import android.graphics.drawable.NinePatchDrawable; import android.os.Build; +import android.os.Handler; +import android.os.Message; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; @@ -81,6 +83,29 @@ public class RecyclerViewDragDropManager { // --- + /** + * Used for listening item drag events + */ + public interface OnItemDragEventListener { + /** + * Callback method to be invoked when dragging is started. + * + * @param position The position of the item. + */ + void onItemDragStarted(int position); + + /** + * Callback method to be invoked when dragging is finished. + * + * @param fromPosition Previous position of the item. + * @param toPosition New position of the item. + * @param result Indicates whether the dragging operation was succeeded. + */ + void onItemDragFinished(int fromPosition, int toPosition, boolean result); + } + + // -- + private static final int SCROLL_DIR_NONE = 0; private static final int SCROLL_DIR_UP = (1 << 0); private static final int SCROLL_DIR_DOWN = (1 << 1); @@ -90,7 +115,6 @@ public class RecyclerViewDragDropManager { private static final float SCROLL_THRESHOLD = 0.3f; // 0.0f < X < 0.5f private static final float SCROLL_AMOUNT_COEFF = 25; - private static final float EDGE_EFFECT_THRESHOLD = 0.5f; // <= 1.0f private static final float SCROLL_TOUCH_SLOP_MULTIPLY = 1.5f; private RecyclerView mRecyclerView; @@ -109,6 +133,8 @@ public class RecyclerViewDragDropManager { private int mScrollTouchSlop; private int mInitialTouchY; private long mInitialTouchItemId = RecyclerView.NO_ID; + private boolean mInitiateOnLongPress; + private boolean mInitiateOnMove = true; private boolean mInScrollByMethod; private int mActualScrollByAmount; @@ -135,6 +161,8 @@ public class RecyclerViewDragDropManager { private int mGrabbedItemHeight; private int mOrigOverScrollMode; private ItemDraggableRange mDraggableRange; + private InternalHandler mHandler; + private OnItemDragEventListener mItemDragEventListener; /** * Constructor. @@ -150,6 +178,11 @@ public class RecyclerViewDragDropManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewDragDropManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + RecyclerViewDragDropManager.this.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } }; mInternalUseOnScrollListener = new RecyclerView.OnScrollListener() { @@ -171,7 +204,6 @@ public class RecyclerViewDragDropManager { * Create wrapped adapter. * * @param adapter The target adapter. - * * @return Wrapped adapter which is associated to this {@link RecyclerViewDragDropManager} instance. */ @SuppressWarnings("unchecked") @@ -204,6 +236,7 @@ public class RecyclerViewDragDropManager { * @param rv The {@link android.support.v7.widget.RecyclerView} instance */ public void attachRecyclerView(RecyclerView rv) { + //noinspection deprecation attachRecyclerView(rv, null); } @@ -214,10 +247,11 @@ public class RecyclerViewDragDropManager { * the wrapped adapter instance which is returned by the * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method. * - * @param rv The {@link android.support.v7.widget.RecyclerView} instance + * @param rv The {@link android.support.v7.widget.RecyclerView} instance * @param scrollEventDistributor The distributor for {@link android.support.v7.widget.RecyclerView.OnScrollListener} event */ - public void attachRecyclerView(RecyclerView rv, RecyclerViewOnScrollEventDistributor scrollEventDistributor) { + @Deprecated + public void attachRecyclerView(RecyclerView rv, @SuppressWarnings("deprecation") RecyclerViewOnScrollEventDistributor scrollEventDistributor) { if (rv == null) { throw new IllegalArgumentException("RecyclerView cannot be null"); } @@ -248,7 +282,7 @@ public class RecyclerViewDragDropManager { scrollEventDistributor.add(mInternalUseOnScrollListener); mScrollEventRegisteredToDistributor = true; } else { - mRecyclerView.setOnScrollListener(mInternalUseOnScrollListener); + mRecyclerView.addOnScrollListener(mInternalUseOnScrollListener); mScrollEventRegisteredToDistributor = false; } @@ -257,6 +291,7 @@ public class RecyclerViewDragDropManager { mDisplayDensity = mRecyclerView.getResources().getDisplayMetrics().density; mTouchSlop = ViewConfiguration.get(mRecyclerView.getContext()).getScaledTouchSlop(); mScrollTouchSlop = (int) (mTouchSlop * SCROLL_TOUCH_SLOP_MULTIPLY + 0.5f); + mHandler = new InternalHandler(this); if (supportsEdgeEffect()) { // edge effect is available on ICS or later @@ -273,6 +308,11 @@ public class RecyclerViewDragDropManager { public void release() { cancelDrag(); + if (mHandler != null) { + mHandler.release(); + mHandler = null; + } + if (mEdgeEffectDecorator != null) { mEdgeEffectDecorator.finish(); mEdgeEffectDecorator = null; @@ -284,7 +324,7 @@ public class RecyclerViewDragDropManager { mInternalUseOnItemTouchListener = null; if (mRecyclerView != null && mInternalUseOnScrollListener != null && mScrollEventRegisteredToDistributor) { - mRecyclerView.setOnScrollListener(null); + mRecyclerView.removeOnScrollListener(mInternalUseOnScrollListener); } mInternalUseOnScrollListener = null; @@ -304,7 +344,7 @@ public class RecyclerViewDragDropManager { * @return True if currently performing item dragging */ public boolean isDragging() { - return (mDraggingItem != null) && (mDeferredCancelProcess == null); + return (mDraggingItemId != RecyclerView.NO_ID) && (mDeferredCancelProcess == null); } /** @@ -325,6 +365,42 @@ public class RecyclerViewDragDropManager { mSwapTargetTranslationInterpolator = interpolator; } + /** + * Returns whether dragging starts on a long press or not. + * + * @return True if dragging starts on a long press, false otherwise. + */ + public boolean isInitiateOnLongPressEnabled() { + return mInitiateOnLongPress; + } + + /** + * Sets whether dragging starts on a long press. (default: false) + * + * @param initiateOnLongPress True to initiate dragging on long press. + */ + public void setInitiateOnLongPress(boolean initiateOnLongPress) { + mInitiateOnLongPress = initiateOnLongPress; + } + + /** + * Returns whether dragging starts on move motions. + * + * @return True if dragging starts on move motions, false otherwise. + */ + public boolean isInitiateOnMoveEnabled() { + return mInitiateOnMove; + } + + /** + * Sets whether dragging starts on move motions. (default: true) + * + * @param initiateOnMove True to initiate dragging on move motions. + */ + public void setInitiateOnMove(boolean initiateOnMove) { + mInitiateOnMove = initiateOnMove; + } + /** * Gets the interpolator which ise used for determining the position of the swapping item. * @@ -334,6 +410,23 @@ public class RecyclerViewDragDropManager { return mSwapTargetTranslationInterpolator; } + /** + * Gets OnItemDragEventListener listener + * @return The listener object + */ + public OnItemDragEventListener getOnItemDragEventListener() { + return mItemDragEventListener; + } + + /** + * Sets OnItemDragEventListener listener + * + * @param listener The listener object + */ + public void setOnItemDragEventListener(OnItemDragEventListener listener) { + mItemDragEventListener = listener; + } + /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { final int action = MotionEventCompat.getActionMasked(e); @@ -393,6 +486,13 @@ public class RecyclerViewDragDropManager { } } + /*package */ void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + cancelDrag(true); + } + } + + /*package*/ void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (LOCAL_LOGV) { Log.v(TAG, "onScrolled(dx = " + dx + ", dy = " + dy + ")"); @@ -410,6 +510,7 @@ public class RecyclerViewDragDropManager { } private boolean handleActionDown(RecyclerView rv, MotionEvent e) { + final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); if (!checkTouchedItemState(rv, holder)) { @@ -419,13 +520,25 @@ public class RecyclerViewDragDropManager { mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); mInitialTouchItemId = holder.getItemId(); + if (mInitiateOnLongPress) { + mHandler.startLongPressDetection(e); + } + return true; } + private void handleOnLongPress(MotionEvent e) { + if (mInitiateOnLongPress) { + checkConditionAndStartDragging(mRecyclerView, e, false); + } + } + @SuppressWarnings("unchecked") private void startDragging(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, ItemDraggableRange range) { safeEndAnimation(rv, holder); + mHandler.cancelLongPressDetection(); + mDraggingItem = holder; // XXX if setIsRecyclable() is used, another view holder objects will be created @@ -461,7 +574,7 @@ public class RecyclerViewDragDropManager { mAdapter.onDragItemStarted(mDraggingItem, mDraggableRange); // setup decorators - mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getPosition()); + mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getLayoutPosition()); mDraggingItemDecorator = new DraggingItemDecorator(mRecyclerView, mDraggingItem, mDraggableRange); mDraggingItemDecorator.setShadowDrawable(mShadowDrawable); @@ -477,6 +590,10 @@ public class RecyclerViewDragDropManager { if (mEdgeEffectDecorator != null) { mEdgeEffectDecorator.reorderToTop(); } + + if (mItemDragEventListener != null) { + mItemDragEventListener.onItemDragStarted(mAdapter.getDraggingItemInitialPosition()); + } } /** @@ -490,7 +607,7 @@ public class RecyclerViewDragDropManager { if (immediately) { finishDragging(false); } else { - if (mDraggingItem != null) { + if (isDragging()) { if (mDeferredCancelProcess == null) { mDeferredCancelProcess = new Runnable() { @Override @@ -510,6 +627,10 @@ public class RecyclerViewDragDropManager { private void finishDragging(boolean result) { final RecyclerView.ViewHolder draggedItem = mDraggingItem; + if (draggedItem == null) { + result = false; + } + // cancel deferred request if (mDeferredCancelProcess != null) { mRecyclerView.removeCallbacks(mDeferredCancelProcess); @@ -543,6 +664,10 @@ public class RecyclerViewDragDropManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); } + if (mRecyclerView != null) { + mRecyclerView.invalidate(); + } + mDraggableRange = null; mDraggingItemDecorator = null; mSwapTargetItemOperator = null; @@ -556,19 +681,34 @@ public class RecyclerViewDragDropManager { mGrabbedPositionY = 0; mGrabbedItemHeight = 0; + + int draggingItemInitialPosition = RecyclerView.NO_POSITION; + int draggingItemCurrentPosition = RecyclerView.NO_POSITION; + // raise onDragItemFinished() event - if ((mAdapter != null) && (draggedItem != null)) { + if (mAdapter != null) { + draggingItemInitialPosition = mAdapter.getDraggingItemInitialPosition(); + draggingItemCurrentPosition = mAdapter.getDraggingItemCurrentPosition(); mAdapter.onDragItemFinished(draggedItem, result); } // if (draggedItem != null) { // draggedItem.setIsRecyclable(true); // } + + if (mItemDragEventListener != null) { + mItemDragEventListener.onItemDragFinished( + draggingItemInitialPosition, + draggingItemCurrentPosition, + result); + } } private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) { final boolean result = (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP); + mHandler.cancelLongPressDetection(); + mInitialTouchY = 0; mLastTouchY = 0; mDragStartTouchY = 0; @@ -588,6 +728,18 @@ public class RecyclerViewDragDropManager { } private boolean handleActionMoveWhileNotDragging(RecyclerView rv, MotionEvent e) { + if (mInitiateOnMove) { + return checkConditionAndStartDragging(rv, e, true); + } else { + return false; + } + } + + private boolean checkConditionAndStartDragging(RecyclerView rv, MotionEvent e, boolean checkTouchSlop) { + if (mDraggingItem != null) { + return false; + } + final int touchX = (int) (e.getX() + 0.5f); final int touchY = (int) (e.getY() + 0.5f); @@ -597,19 +749,24 @@ public class RecyclerViewDragDropManager { return false; } - if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) { - return false; + if (checkTouchSlop) { + if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) { + return false; + } } final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); - if (!checkTouchedItemState(rv, holder)) { + if (!checkTouchedItemState(rv, holder) || holder.getItemId() != mInitialTouchItemId) { mInitialTouchItemId = RecyclerView.NO_ID; + mHandler.cancelLongPressDetection(); + return false; } - if (holder.getItemId() != mInitialTouchItemId) { - mInitialTouchItemId = RecyclerView.NO_ID; + int position = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (position == RecyclerView.NO_POSITION) { return false; } @@ -619,11 +776,11 @@ public class RecyclerViewDragDropManager { final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - if (!mAdapter.canStartDrag(holder, viewX, viewY)) { + if (!mAdapter.canStartDrag(holder, position, viewX, viewY)) { return false; } - ItemDraggableRange range = mAdapter.getItemDraggableRange(holder); + ItemDraggableRange range = mAdapter.getItemDraggableRange(holder, position); if (range == null) { range = new ItemDraggableRange(0, Math.max(0, mAdapter.getItemCount() - 1)); @@ -639,7 +796,6 @@ public class RecyclerViewDragDropManager { startDragging(rv, e, holder, range); return true; - } private void verifyItemDraggableRange(ItemDraggableRange range, RecyclerView.ViewHolder holder) { @@ -658,14 +814,15 @@ public class RecyclerViewDragDropManager { throw new IllegalStateException("Invalid range specified --- end >= count (range = " + range + ")"); } - if (!range.checkInRange(holder.getPosition())) { + if (!range.checkInRange(holder.getAdapterPosition())) { throw new IllegalStateException( "Invalid range specified --- does not contain drag target item" - + " (range = " + range + ", position = " + holder.getPosition() + ")"); + + " (range = " + range + ", position = " + holder.getAdapterPosition() + ")"); } } private void handleActionMoveWhileDragging(RecyclerView rv, MotionEvent e) { + mLastTouchY = (int) (e.getY() + 0.5f); mDragMinTouchY = Math.min(mDragMinTouchY, mLastTouchY); mDragMaxTouchY = Math.max(mDragMaxTouchY, mLastTouchY); @@ -765,6 +922,7 @@ public class RecyclerViewDragDropManager { } } + // scroll if ((!reachedToTopHardLimit && (scrollAmount < 0)) || (!reachedToBottomHardLimit && (scrollAmount > 0))) { safeEndAnimations(rv); @@ -788,34 +946,67 @@ public class RecyclerViewDragDropManager { if (mEdgeEffectDecorator != null) { + final float edgeEffectStrength = 0.005f; + final int draggingItemTop = mDraggingItemDecorator.getTranslatedItemPositionTop(); final int draggingItemBottom = mDraggingItemDecorator.getTranslatedItemPositionBottom(); final int draggingItemCenter = (draggingItemTop + draggingItemBottom) / 2; - final int nearEdgePosition = ((draggingItemCenter < (height / 2)) ? draggingItemTop : draggingItemBottom); + final int nearEdgePosition; + + if (firstVisibleChild == 0 && lastVisibleChild == 0) { + // has only 1 item + nearEdgePosition = (scrollAmount < 0) ? draggingItemTop : draggingItemBottom; + } else { + nearEdgePosition = (draggingItemCenter < (height / 2)) ? draggingItemTop : draggingItemBottom; + } final float nearEdgeOffset = (nearEdgePosition * invHeight) - 0.5f; final float absNearEdgeOffset = Math.abs(nearEdgeOffset); - final float edgeEffectAcceleration = Math.max(0.0f, threshold - (0.5f - absNearEdgeOffset)) * invThreshold; + float edgeEffectPullDistance = 0; - if ((edgeEffectAcceleration >= EDGE_EFFECT_THRESHOLD) && (scrollAmount != 0) && !actualIsScrolling) { - // over scrolled - final float distance = edgeEffectAcceleration * 0.02f; - - if (scrollAmount < 0) { + if ((absNearEdgeOffset > 0.4f) && (scrollAmount != 0) && !actualIsScrolling) { + if (nearEdgeOffset < 0) { // upward - mEdgeEffectDecorator.pullTopGlow(distance); + if (mDraggingItemDecorator.isReachedToTopLimit()) { + edgeEffectPullDistance = -mDisplayDensity * edgeEffectStrength; + } } else { // downward - mEdgeEffectDecorator.pullBottom(distance); + if (mDraggingItemDecorator.isReachedToBottomLimit()) { + edgeEffectPullDistance = mDisplayDensity * edgeEffectStrength; + } } - } else { - mEdgeEffectDecorator.releaseBothGlows(); } + + updateEdgeEffect(edgeEffectPullDistance); } - checkItemSwapping(rv); + ViewCompat.postOnAnimation(mRecyclerView, mCheckItemSwappingRunnable); } + private void updateEdgeEffect(float distance) { + if (distance != 0.0f) { + if (distance < 0) { + // upward + mEdgeEffectDecorator.pullTopGlow(distance); + } else { + // downward + mEdgeEffectDecorator.pullBottom(distance); + } + } else { + mEdgeEffectDecorator.releaseBothGlows(); + } + } + + private Runnable mCheckItemSwappingRunnable = new Runnable() { + @Override + public void run() { + if (mDraggingItem != null) { + checkItemSwapping(mRecyclerView); + } + } + }; + private int scrollByYAndGetScrolledAmount(int ry) { // NOTE: mActualScrollByAmount --- Hackish! To detect over scrolling. @@ -843,8 +1034,8 @@ public class RecyclerViewDragDropManager { private void swapItems(RecyclerView rv, RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetHolder) { final Rect swapTargetMargins = CustomRecyclerViewUtils.getLayoutMargins(swapTargetHolder.itemView, mTmpRect1); - final int fromPosition = draggingItem.getPosition(); - final int toPosition = swapTargetHolder.getPosition(); + final int fromPosition = draggingItem.getAdapterPosition(); + final int toPosition = swapTargetHolder.getAdapterPosition(); final int diffPosition = Math.abs(fromPosition - toPosition); boolean performSwapping = false; @@ -903,11 +1094,13 @@ public class RecyclerViewDragDropManager { firstVisibleTopItem = rv.getChildViewHolder(child); } } - final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getPosition() : RecyclerView.NO_POSITION; - + final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getAdapterPosition() : RecyclerView.NO_POSITION; + // NOTE: This method invokes notifyItemMoved() method internally. Be careful! mAdapter.moveItem(fromPosition, toPosition); + safeEndAnimations(rv); + if (fromPosition == prevTopItemPosition) { //noinspection UnnecessaryLocalVariable final Rect margins = swapTargetMargins; @@ -933,7 +1126,7 @@ public class RecyclerViewDragDropManager { return false; } - final int itemPosition = holder.getPosition(); + final int itemPosition = holder.getAdapterPosition(); final RecyclerView.Adapter adapter = rv.getAdapter(); // verify the touched item is valid state @@ -975,7 +1168,7 @@ public class RecyclerViewDragDropManager { static RecyclerView.ViewHolder findSwapTargetItem( RecyclerView rv, RecyclerView.ViewHolder draggingItem, long draggingItemId, int overlayItemTop, ItemDraggableRange range) { - final int draggingItemPosition = draggingItem.getPosition(); + final int draggingItemPosition = draggingItem.getAdapterPosition(); final int draggingViewTop = draggingItem.itemView.getTop(); RecyclerView.ViewHolder swapTargetHolder = null; @@ -984,18 +1177,18 @@ public class RecyclerViewDragDropManager { draggingItem.getItemId() == draggingItemId) { if (overlayItemTop < draggingViewTop) { if (draggingItemPosition > 0) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition - 1); + swapTargetHolder = rv.findViewHolderForAdapterPosition(draggingItemPosition - 1); } } else if (overlayItemTop > draggingViewTop) { if (draggingItemPosition < (rv.getAdapter().getItemCount() - 1)) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition + 1); + swapTargetHolder = rv.findViewHolderForAdapterPosition(draggingItemPosition + 1); } } } // check range if (swapTargetHolder != null && range != null) { - if (!range.checkInRange(swapTargetHolder.getPosition())) { + if (!range.checkInRange(swapTargetHolder.getAdapterPosition())) { swapTargetHolder = null; } } @@ -1039,6 +1232,15 @@ public class RecyclerViewDragDropManager { return mItemSettleBackIntoPlaceAnimationInterpolator; } + /*package*/ void onDraggingItemViewRecycled() { + mDraggingItemDecorator.invalidateDraggingItem(); + } + + /*package*/ void onNewDraggingItemViewBinded(RecyclerView.ViewHolder holder) { + mDraggingItem = holder; + mDraggingItemDecorator.setDraggingItemViewHolder(holder); + } + private static class ScrollOnDraggingProcessRunnable implements Runnable { private final WeakReference mHolderRef; private boolean mStarted; @@ -1107,4 +1309,44 @@ public class RecyclerViewDragDropManager { } } } + + private static class InternalHandler extends Handler { + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();// + ViewConfiguration.getTapTimeout(); + private static final int MSG_LONGPRESS = 1; + + private RecyclerViewDragDropManager mHolder; + private MotionEvent mDownMotionEvent; + + public InternalHandler(RecyclerViewDragDropManager holder) { + mHolder = holder; + } + + public void release() { + removeCallbacks(null); + mHolder = null; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LONGPRESS: + mHolder.handleOnLongPress(mDownMotionEvent); + break; + } + } + + public void startLongPressDetection(MotionEvent e) { + cancelLongPressDetection(); + mDownMotionEvent = MotionEvent.obtain(e); + sendEmptyMessageAtTime(MSG_LONGPRESS, e.getDownTime() + LONGPRESS_TIMEOUT); + } + + public void cancelLongPressDetection() { + removeMessages(MSG_LONGPRESS); + if (mDownMotionEvent != null) { + mDownMotionEvent.recycle(); + mDownMotionEvent = null; + } + } + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java old mode 100644 new mode 100755 index 66e58c69121..d88a24aa9b7 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java @@ -94,8 +94,8 @@ class SwapTargetItemOperator extends BaseDraggableItemDecorator { private float calculateTranslationPhase(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem) { final View swapItemView = swapTargetItem.itemView; - final int pos1 = draggingItem.getPosition(); - final int pos2 = swapTargetItem.getPosition(); + final int pos1 = draggingItem.getLayoutPosition(); + final int pos2 = swapTargetItem.getLayoutPosition(); CustomRecyclerViewUtils.getDecorationOffsets( mRecyclerView.getLayoutManager(), swapItemView, mSwapTargetDecorationOffsets); @@ -132,8 +132,8 @@ class SwapTargetItemOperator extends BaseDraggableItemDecorator { private void updateSwapTargetTranslation(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem, float translationPhase) { final View swapItemView = swapTargetItem.itemView; - final int pos1 = draggingItem.getPosition(); - final int pos2 = swapTargetItem.getPosition(); + final int pos1 = draggingItem.getLayoutPosition(); + final int pos2 = swapTargetItem.getLayoutPosition(); final Rect m1 = mDraggingItemMargins; final Rect d1 = mDraggingItemDecorationOffsets; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java old mode 100644 new mode 100755 index fb9ff5d79d9..a98ed263356 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java @@ -20,6 +20,12 @@ import android.support.v7.widget.RecyclerView; import java.lang.ref.WeakReference; +/** + * Deprecated. + * + * Please use {@link RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)} and {@link RecyclerView#removeOnScrollListener(RecyclerView.OnScrollListener)} instead. + */ +@Deprecated public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventDistributor { private InternalOnScrollListener mInternalOnScrollListener; @@ -34,17 +40,20 @@ public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventD protected void onRecyclerViewAttached(RecyclerView rv) { super.onRecyclerViewAttached(rv); - rv.setOnScrollListener(mInternalOnScrollListener); + rv.addOnScrollListener(mInternalOnScrollListener); } @Override protected void onRelease() { - super.onRelease(); - if (mInternalOnScrollListener != null) { + if (mRecyclerView != null) { + mRecyclerView.removeOnScrollListener(mInternalOnScrollListener); + } mInternalOnScrollListener.release(); mInternalOnScrollListener = null; } + + super.onRelease(); } /*package*/ void handleOnScrollStateChanged(RecyclerView recyclerView, int newState) { @@ -67,6 +76,7 @@ public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventD } } + @SuppressWarnings("deprecation") private static class InternalOnScrollListener extends RecyclerView.OnScrollListener { private WeakReference mRefDistributor; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java new file mode 100755 index 00000000000..0d4f0da0d13 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Haruki Hasegawa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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 com.h6ah4i.android.widget.advrecyclerview.expandable; + +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; + +public class ChildPositionItemDraggableRange extends ItemDraggableRange { + public ChildPositionItemDraggableRange(int start, int end) { + super(start, end); + } + + protected String getClassName() { + return "ChildPositionItemDraggableRange"; + } +} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java old mode 100644 new mode 100755 index cc8cef577c8..8fb1de8e28a --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java @@ -125,7 +125,7 @@ public interface ExpandableItemAdapter> 32); + final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK); if (id2 < id1) { index = j; + + if (adapter == null || adapter.onHookGroupCollapse(position, fromUser)) { + if (collapseGroup(position)) { + if (collapseListener != null) { + collapseListener.onGroupCollapse(position, fromUser); + } + } + } } else if (id2 == id1) { // matched - final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK); index = j + 1; - expandGroup(position); - break; + + if (adapter == null || adapter.onHookGroupExpand(position, fromUser)) { + if (expandGroup(position)) { + if (expandListener != null) { + expandListener.onGroupExpand(position, fromUser); + } + } + } } else { // id2 > id1 break; } } } + + if (adapter != null || collapseListener != null) { + for (int i = index; i < idAndPos.length; i++) { + final int id2 = (int) (idAndPos[i] >> 32); + final int position = (int) (idAndPos[i] & LOWER_31BIT_MASK); + + if (adapter == null || adapter.onHookGroupCollapse(position, fromUser)) { + if (collapseGroup(position)) { + if (collapseListener != null) { + collapseListener.onGroupCollapse(position, fromUser); + } + } + } + } + } } public int[] getSavedStateArray() { - int [] expandedGroups = new int[mExpandedGroupCount]; + int[] expandedGroups = new int[mExpandedGroupCount]; int index = 0; for (int i = 0; i < mGroupCount; i++) { @@ -132,6 +165,8 @@ class ExpandablePositionTranslator { throw new IllegalStateException("may be a bug (index = " + index + ", mExpandedGroupCount = " + mExpandedGroupCount + ")"); } + Arrays.sort(expandedGroups); + return expandedGroups; } @@ -165,14 +200,11 @@ class ExpandablePositionTranslator { mCachedGroupPosInfo[groupPosition] &= (~FLAG_EXPANDED); mExpandedGroupCount -= 1; - if (childCount > 0) { - mExpandedChildCount -= childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeRemoved() - return true; - } else { - return false; - } + mExpandedChildCount -= childCount; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + + // requires notifyItemRangeRemoved() + return true; } public boolean expandGroup(int groupPosition) { @@ -185,14 +217,11 @@ class ExpandablePositionTranslator { mCachedGroupPosInfo[groupPosition] |= FLAG_EXPANDED; mExpandedGroupCount += 1; - if (childCount > 0) { - mExpandedChildCount += childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeInserted() - return true; - } else { - return false; - } + mExpandedChildCount += childCount; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + + // requires notifyItemRangeInserted() + return true; } public void moveGroupItem(int fromGroupPosition, int toGroupPosition) { @@ -223,7 +252,7 @@ class ExpandablePositionTranslator { final int minPosition = Math.min(fromGroupPosition, toGroupPosition); if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, minPosition - 1); } else { mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; } @@ -258,7 +287,7 @@ class ExpandablePositionTranslator { final int minPosition = Math.min(fromGroupPosition, toGroupPosition); if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, minPosition - 1); } else { mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; } @@ -330,7 +359,7 @@ class ExpandablePositionTranslator { } // final int startIndex = 0; - final int startIndex = Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition); + final int startIndex = Math.max(0, Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition)); int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition; int offset = (int) (mCachedGroupPosInfo[startIndex] >>> 32); int flatPosition = RecyclerView.NO_POSITION; @@ -399,30 +428,155 @@ class ExpandablePositionTranslator { return lastS; } - public void removeGroupItem(int groupPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; + public void removeChildItem(int groupPosition, int childPosition) { + removeChildItems(groupPosition, childPosition, 1); + } - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= (int) (t & LOWER_31BIT_MASK); - mExpandedGroupCount -= 1; + public void removeChildItems(int groupPosition, int childPositionStart, int count) { + final long t = mCachedGroupPosInfo[groupPosition]; + final int curCount = (int) (t & LOWER_31BIT_MASK); + + if (!((childPositionStart >= 0) && ((childPositionStart + count) <= curCount))) { + throw new IllegalStateException( + "Invalid child position " + + "removeChildItems(groupPosition = " + groupPosition + ", childPosition = " + childPositionStart + ", count = " + count + ")"); } - mGroupCount -= 1; + if ((t & FLAG_EXPANDED) != 0) { + mExpandedChildCount -= count; + } + + mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (curCount - count); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition - 1); + } + + public void insertChildItem(int groupPosition, int childPosition) { + insertChildItems(groupPosition, childPosition, 1); + } + + public void insertChildItems(int groupPosition, int childPositionStart, int count) { + final long t = mCachedGroupPosInfo[groupPosition]; + final int curCount = (int) (t & LOWER_31BIT_MASK); + + if (!((childPositionStart >= 0) && (childPositionStart <= curCount))) { + throw new IllegalStateException( + "Invalid child position " + + "insertChildItems(groupPosition = " + groupPosition + ", childPositionStart = " + childPositionStart + ", count = " + count + ")"); + } + + if ((t & FLAG_EXPANDED) != 0) { + mExpandedChildCount += count; + } + + mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (curCount + count); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + } + + public int insertGroupItems(int groupPosition, int count) { + if (count <= 0) { + return 0; + } + + final int n = count; + + enlargeArraysIfNeeded(mGroupCount + n, true); + + // shift to backward + final ExpandableItemAdapter adapter = mAdapter; + final long[] info = mCachedGroupPosInfo; + final int[] ids = mCachedGroupId; + + int start = mGroupCount - 1 + n; + int end = groupPosition - 1 + n; + for (int i = start; i > end; i--) { + info[i] = info[i - n]; + ids[i] = ids[i - n]; + } + + int end2 = groupPosition + n; + for (int i = groupPosition; i < end2; i++) { + final long groupId = adapter.getGroupId(i); + final int childCount = adapter.getChildCount(i); + + info[i] = (((long) i << 32) | childCount); + ids[i] = (int) (groupId & LOWER_32BIT_MASK); + } + + mGroupCount += n; + + int calculatedOffset = (mGroupCount == 0) ? RecyclerView.NO_POSITION : (groupPosition - 1); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, calculatedOffset); + + return n; + } + + public int insertGroupItem(int groupPosition) { + return insertGroupItems(groupPosition, 1); + } + + public int removeGroupItems(int groupPosition, int count) { + if (count <= 0) { + return 0; + } + + final int n = count; + int removedVisibleItemCount = 0; + + for (int i = 0; i < n; i++) { + final long t = mCachedGroupPosInfo[groupPosition + i]; + + if ((t & FLAG_EXPANDED) != 0) { + int visibleChildCount = (int) (t & LOWER_31BIT_MASK); + removedVisibleItemCount += visibleChildCount; + mExpandedChildCount -= visibleChildCount; + mExpandedGroupCount -= 1; + } + } + removedVisibleItemCount += n; + mGroupCount -= n; // shift to forward for (int i = groupPosition; i < mGroupCount; i++) { - mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1]; - mCachedGroupId[i] = mCachedGroupId[i + 1]; + mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + n]; + mCachedGroupId[i] = mCachedGroupId[i + n]; } + + int calculatedOffset = (mGroupCount == 0) ? RecyclerView.NO_POSITION : (groupPosition - 1); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, calculatedOffset); + + return removedVisibleItemCount; } - public void removeChildItem(int groupPosition, int childPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; + public int removeGroupItem(int groupPosition) { + return removeGroupItems(groupPosition, 1); + } - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= 1; + + private void enlargeArraysIfNeeded(int size, boolean preserveData) { + int allocSize = (size + (2 * ALLOCATE_UNIT - 1)) & ~(ALLOCATE_UNIT - 1); + + long[] curInfo = mCachedGroupPosInfo; + int[] curId = mCachedGroupId; + long[] newInfo = curInfo; + int[] newId = curId; + + if (curInfo == null || curInfo.length < size) { + newInfo = new long[allocSize]; + } + if (curId == null || curId.length < size) { + newId = new int[allocSize]; } - mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | ((t & LOWER_31BIT_MASK) - 1); + if (preserveData) { + if (curInfo != null && curInfo != newInfo) { + System.arraycopy(curInfo, 0, newInfo, 0, curInfo.length); + } + if (curId != null && curId != newId) { + System.arraycopy(curId, 0, newId, 0, curId.length); + } + } + + mCachedGroupPosInfo = newInfo; + mCachedGroupId = newId; } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java old mode 100644 new mode 100755 index 0d8a3e9b42f..848acecfd42 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java @@ -41,7 +41,13 @@ class ExpandableRecyclerViewWrapperAdapter private ExpandableItemAdapter mExpandableItemAdapter; private RecyclerViewExpandableItemManager mExpandableListManager; private ExpandablePositionTranslator mPositionTranslator; - private int mDraggingItemGroupPosition = RecyclerView.NO_POSITION; + private int mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + private int mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + private int mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + private int mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; + + private RecyclerViewExpandableItemManager.OnGroupExpandListener mOnGroupExpandListener; + private RecyclerViewExpandableItemManager.OnGroupCollapseListener mOnGroupCollapseListener; public ExpandableRecyclerViewWrapperAdapter(RecyclerViewExpandableItemManager manager, RecyclerView.Adapter adapter, int[] expandedItemsSavedState) { super(adapter); @@ -61,7 +67,8 @@ class ExpandableRecyclerViewWrapperAdapter mPositionTranslator.build(mExpandableItemAdapter); if (expandedItemsSavedState != null) { - mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState); + // NOTE: do not call hook routines and listener methods + mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState, null, null, null); } } @@ -71,6 +78,8 @@ class ExpandableRecyclerViewWrapperAdapter mExpandableItemAdapter = null; mExpandableListManager = null; + mOnGroupExpandListener = null; + mOnGroupCollapseListener = null; } @Override @@ -174,7 +183,7 @@ class ExpandableRecyclerViewWrapperAdapter safeUpdateExpandStateFlags(holder, flags); - correctItemDragStateFlags(holder, groupPosition); + correctItemDragStateFlags(holder, groupPosition, childPosition); if (childPosition == RecyclerView.NO_POSITION) { mExpandableItemAdapter.onBindGroupViewHolder(holder, groupPosition, viewType); @@ -187,7 +196,9 @@ class ExpandableRecyclerViewWrapperAdapter if (mPositionTranslator != null) { int [] savedState = mPositionTranslator.getSavedStateArray(); mPositionTranslator.build(mExpandableItemAdapter); - mPositionTranslator.restoreExpandedGroupItems(savedState); + + // NOTE: do not call hook routines and listener methods + mPositionTranslator.restoreExpandedGroupItems(savedState, null, null, null); } } @@ -235,14 +246,14 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int x, int y) { + public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int position, int x, int y) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return false; } final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - final int flatPosition = holder.getPosition(); + final int flatPosition = position; final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -254,14 +265,17 @@ class ExpandableRecyclerViewWrapperAdapter canStart = adapter.onCheckChildCanStartDrag(holder, groupPosition, childPosition, x, y); } - mDraggingItemGroupPosition = (canStart) ? groupPosition : RecyclerView.NO_POSITION; + mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; return canStart; } @SuppressWarnings("unchecked") @Override - public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder) { + public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder, int position) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return null; } @@ -272,66 +286,109 @@ class ExpandableRecyclerViewWrapperAdapter final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - final int flatPosition = holder.getPosition(); + final int flatPosition = position; final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); if (childPosition == RecyclerView.NO_POSITION) { // group - final ItemDraggableRange groupRange = adapter.onGetGroupItemDraggableRange(holder, groupPosition); + final ItemDraggableRange range = adapter.onGetGroupItemDraggableRange(holder, groupPosition); - if (groupRange == null) { + if (range == null) { final int lastGroup = Math.max(0, mExpandableItemAdapter.getGroupCount() - 1); final int start = 0; final int end = Math.max(start, mPositionTranslator.getItemCount() - mPositionTranslator.getVisibleChildCount(lastGroup) - 1); return new ItemDraggableRange(start, end); - } else { - final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart()); - final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd()); + } else if (isGroupPositionRange(range)) { + final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getStart()); + final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getEnd()); final int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition); int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition); - if (groupRange.getEnd() > groupPosition) { - end += mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); + if (range.getEnd() > groupPosition) { + end += mPositionTranslator.getVisibleChildCount(range.getEnd()); } + mDraggingItemGroupRangeStart = range.getStart(); + mDraggingItemGroupRangeEnd = range.getEnd(); + return new ItemDraggableRange(start, end); + } else { + throw new IllegalStateException("Invalid range specified: " + range); } } else { // child - final ItemDraggableRange groupRange = adapter.onGetChildItemDraggableRange(holder, groupPosition, childPosition); + final ItemDraggableRange range = adapter.onGetChildItemDraggableRange(holder, groupPosition, childPosition); // NOTE: // This method returns actual drag-sortable range, but the visual drag-sortable range would be different. // Thus appending the STATE_FLAG_IS_IN_RANGE flag at correctItemDragStateFlags() to avoid visual corruption. - if (groupRange == null) { + if (range == null) { final int start = 1; // 1 --- to avoid swapping with the first group item return new ItemDraggableRange(start, Math.max(start, mPositionTranslator.getItemCount() - 1)); - } else { - final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart()); - final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd()); + } else if (isGroupPositionRange(range)) { + final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getStart()); + final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getEnd()); final int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition) - + mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); + + mPositionTranslator.getVisibleChildCount(range.getEnd()); int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition) + 1; start = Math.min(start, end); + mDraggingItemGroupRangeStart = range.getStart(); + mDraggingItemGroupRangeEnd = range.getEnd(); + return new ItemDraggableRange(start, end); + } else if (isChildPositionRange(range)) { + final int maxChildrenPos = Math.max(mPositionTranslator.getVisibleChildCount(groupPosition) - 1, 0); + final int childStart = Math.min(range.getStart(), maxChildrenPos); + final int childEnd = Math.min(range.getEnd(), maxChildrenPos); + final long startPackedChildPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childStart); + final long endPackedChildPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childEnd); + final int start = mPositionTranslator.getFlatPosition(startPackedChildPosition); + final int end = mPositionTranslator.getFlatPosition(endPackedChildPosition); + + mDraggingItemChildRangeStart = childStart; + mDraggingItemChildRangeEnd = childEnd; + + return new ItemDraggableRange(start, end); + } else { + throw new IllegalStateException("Invalid range specified: " + range); } } } + private static boolean isGroupPositionRange(ItemDraggableRange range) { + if (range.getClass().equals(GroupPositionItemDraggableRange.class)) { + return true; + } else if (range.getClass().equals(ItemDraggableRange.class)) { + // NOTE: ItemDraggableRange is regarded as group position + return true; + } + return false; + } + + private static boolean isChildPositionRange(ItemDraggableRange range) { + if (range.getClass().equals(ChildPositionItemDraggableRange.class)) { + return true; + } + return false; + } + @Override public void onMoveItem(int fromPosition, int toPosition) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return; } - mDraggingItemGroupPosition = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; if (fromPosition == toPosition) { return; @@ -436,14 +493,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) { + public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, int position, int x, int y) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return RecyclerViewSwipeManager.REACTION_CAN_NOT_SWIPE_BOTH; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -456,14 +514,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int type) { + public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int position, int type) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -476,14 +535,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public int onSwipeItem(RecyclerView.ViewHolder holder, int result) { + public int onSwipeItem(RecyclerView.ViewHolder holder, int position, int result) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -496,14 +556,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int result, int reaction) { + public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int position, int result, int reaction) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -513,16 +574,17 @@ class ExpandableRecyclerViewWrapperAdapter adapter.onPerformAfterSwipeChildReaction(holder, groupPosition, childPosition, result, reaction); } } + // NOTE: This method is called from RecyclerViewExpandableItemManager /*package*/ @SuppressWarnings("unchecked") - boolean onTapItem(RecyclerView.ViewHolder holder, int x, int y) { + boolean onTapItem(RecyclerView.ViewHolder holder, int position, int x, int y) { if (mExpandableItemAdapter == null) { return false; } - final int position = holder.getPosition(); - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -539,19 +601,24 @@ class ExpandableRecyclerViewWrapperAdapter } if (expand) { - expandGroup(groupPosition); + expandGroup(groupPosition, true); } else { - collapseGroup(groupPosition); + collapseGroup(groupPosition, true); } return true; } - /*package*/ boolean collapseGroup(int groupPosition) { + /*package*/ boolean collapseGroup(int groupPosition, boolean fromUser) { if (!mPositionTranslator.isGroupExpanded(groupPosition)) { return false; } + // call hook method + if (!mExpandableItemAdapter.onHookGroupCollapse(groupPosition, fromUser)) { + return false; + } + if (mPositionTranslator.collapseGroup(groupPosition)) { final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); @@ -568,14 +635,23 @@ class ExpandableRecyclerViewWrapperAdapter notifyItemChanged(flatPosition); } + // raise onGroupCollapse() event + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(groupPosition, fromUser); + } + return true; } - /*package*/ boolean expandGroup(int groupPosition) { + /*package*/ boolean expandGroup(int groupPosition, boolean fromUser) { if (mPositionTranslator.isGroupExpanded(groupPosition)) { return false; } + // call hook method + if (!mExpandableItemAdapter.onHookGroupExpand(groupPosition, fromUser)) { + return false; + } if (mPositionTranslator.expandGroup(groupPosition)) { final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); @@ -592,6 +668,11 @@ class ExpandableRecyclerViewWrapperAdapter notifyItemChanged(flatPosition); } + // raise onGroupExpand() event + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(groupPosition, fromUser); + } + return true; } @@ -615,6 +696,161 @@ class ExpandableRecyclerViewWrapperAdapter } } + /*package*/ void setOnGroupExpandListener(RecyclerViewExpandableItemManager.OnGroupExpandListener listener) { + mOnGroupExpandListener = listener; + } + + /*package*/ void setOnGroupCollapseListener(RecyclerViewExpandableItemManager.OnGroupCollapseListener listener) { + mOnGroupCollapseListener = listener; + } + + /*package*/ void restoreState(int[] adapterSavedState, boolean callHook, boolean callListeners) { + mPositionTranslator.restoreExpandedGroupItems( + adapterSavedState, + (callHook ? mExpandableItemAdapter : null), + (callListeners ? mOnGroupExpandListener : null), + (callListeners ? mOnGroupCollapseListener : null)); + } + + /*package*/ void notifyGroupItemChanged(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemChanged(flatPosition); + } + } + + /*package*/ void notifyGroupAndChildrenItemsChanged(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeChanged(flatPosition, 1 + visibleChildCount); + } + } + + /*package*/ void notifyChildrenOfGroupItemChanged(int groupPosition) { + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + // notify if the group is expanded + if (visibleChildCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, 0); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeChanged(flatPosition, visibleChildCount); + } + } + } + + /*package*/ void notifyChildItemChanged(int groupPosition, int childPosition) { + notifyChildItemRangeChanged(groupPosition, childPosition, 1); + } + + /*package*/ void notifyChildItemRangeChanged(int groupPosition, int childPositionStart, int itemCount) { + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + // notify if the group is expanded + if ((visibleChildCount > 0) && (childPositionStart < visibleChildCount)) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, 0); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + final int startPosition = flatPosition + childPositionStart; + final int count = Math.min(itemCount, (visibleChildCount - childPositionStart)); + + notifyItemRangeChanged(startPosition, count); + } + } + } + + /*package*/ void notifyChildItemInserted(int groupPosition, int childPosition) { + mPositionTranslator.insertChildItem(groupPosition, childPosition); + + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemInserted(flatPosition); + } + } + + /*package*/ void notifyChildItemRangeInserted(int groupPosition, int childPositionStart, int itemCount) { + mPositionTranslator.insertChildItems(groupPosition, childPositionStart, itemCount); + + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeInserted(flatPosition, itemCount); + } + } + + /*package*/ void notifyChildItemRemoved(int groupPosition, int childPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + mPositionTranslator.removeChildItem(groupPosition, childPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRemoved(flatPosition); + } + } + + /*package*/ void notifyChildItemRangeRemoved(int groupPosition, int childPositionStart, int itemCount) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + mPositionTranslator.removeChildItems(groupPosition, childPositionStart, itemCount); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeRemoved(flatPosition, itemCount); + } + } + + /*package*/ void notifyGroupItemInserted(int groupPosition) { + int insertedCount = mPositionTranslator.insertGroupItem(groupPosition); + if (insertedCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + notifyItemInserted(flatPosition); + } + } + + /*package*/ void notifyGroupItemRangeInserted(int groupPositionStart, int count) { + int insertedCount = mPositionTranslator.insertGroupItems(groupPositionStart, count); + if (insertedCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + notifyItemRangeInserted(flatPosition, insertedCount); + } + } + + /*package*/ void notifyGroupItemRemoved(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + int removedCount = mPositionTranslator.removeGroupItem(groupPosition); + if (removedCount > 0) { + notifyItemRangeRemoved(flatPosition, removedCount); + } + } + + /*package*/ void notifyGroupItemRangeRemoved(int groupPositionStart, int count) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + int removedCount = mPositionTranslator.removeGroupItems(groupPositionStart, count); + if (removedCount > 0) { + notifyItemRangeRemoved(flatPosition, removedCount); + } + } + + private static ExpandableItemAdapter getExpandableItemAdapter(RecyclerView.Adapter adapter) { return WrapperAdapterUtils.findWrappedAdapter(adapter, ExpandableItemAdapter.class); } @@ -637,22 +873,43 @@ class ExpandableRecyclerViewWrapperAdapter holder2.setExpandStateFlags(flags); } - private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition) { + private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition, int childPosition) { if (!(holder instanceof DraggableItemViewHolder)) { return; } final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder; + final boolean groupRangeSpecified = + (mDraggingItemGroupRangeStart != RecyclerView.NO_POSITION) && + (mDraggingItemGroupRangeEnd != RecyclerView.NO_POSITION); + final boolean childRangeSpecified = + (mDraggingItemChildRangeStart != RecyclerView.NO_POSITION) && + (mDraggingItemChildRangeEnd != RecyclerView.NO_POSITION); + final boolean isInGroupRange = + (groupPosition >= mDraggingItemGroupRangeStart) && + (groupPosition <= mDraggingItemGroupRangeEnd); + final boolean isInChildRange = + (groupPosition != RecyclerView.NO_POSITION) && + (childPosition >= mDraggingItemChildRangeStart) && + (childPosition <= mDraggingItemChildRangeEnd); + final int flags = holder2.getDragStateFlags(); + boolean needCorrection = false; if (((flags & RecyclerViewDragDropManager.STATE_FLAG_DRAGGING) != 0) && ((flags & RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE) == 0)) { - if (mDraggingItemGroupPosition == groupPosition) { - holder2.setDragStateFlags( - flags | RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE | - RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED); + if (!groupRangeSpecified || isInGroupRange) { + if (!childRangeSpecified || (childRangeSpecified && isInChildRange)) { + needCorrection = true; + } } } + + if (needCorrection) { + holder2.setDragStateFlags( + flags | RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE | + RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED); + } } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java new file mode 100755 index 00000000000..301b1561e13 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Haruki Hasegawa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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 com.h6ah4i.android.widget.advrecyclerview.expandable; + +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; + +public class GroupPositionItemDraggableRange extends ItemDraggableRange { + public GroupPositionItemDraggableRange(int start, int end) { + super(start, end); + } + + protected String getClassName() { + return "GroupPositionItemDraggableRange"; + } +} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java old mode 100644 new mode 100755 index 206bf9d81e6..20ee11b90d6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java @@ -64,6 +64,35 @@ public class RecyclerViewExpandableItemManager { */ public static final int STATE_FLAG_IS_UPDATED = (1 << 31); + + // --- + + /** + * Used for being notified when a group is expanded + */ + public interface OnGroupExpandListener { + /** + * Callback method to be invoked when a group in this expandable list has been expanded. + * + * @param groupPosition The group position that was expanded + * @param fromUser Whether the expand request is issued by a user operation + */ + void onGroupExpand(int groupPosition, boolean fromUser); + } + + /** + * Used for being notified when a group is collapsed + */ + public interface OnGroupCollapseListener { + /** + * Callback method to be invoked when a group in this expandable list has been collapsed. + * + * @param groupPosition The group position that was collapsed + * @param fromUser Whether the collapse request is issued by a user operation + */ + void onGroupCollapse(int groupPosition, boolean fromUser); + } + // --- private SavedState mSavedState; @@ -71,6 +100,8 @@ public class RecyclerViewExpandableItemManager { private RecyclerView mRecyclerView; private ExpandableRecyclerViewWrapperAdapter mAdapter; private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; + private OnGroupExpandListener mOnGroupExpandListener; + private OnGroupCollapseListener mOnGroupCollapseListener; private long mTouchedItemId = RecyclerView.NO_ID; private int mTouchSlop; @@ -92,6 +123,10 @@ public class RecyclerViewExpandableItemManager { @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; if (savedState instanceof SavedState) { @@ -145,6 +180,8 @@ public class RecyclerViewExpandableItemManager { mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); } mInternalUseOnItemTouchListener = null; + mOnGroupExpandListener = null; + mOnGroupCollapseListener = null; mRecyclerView = null; mSavedState = null; } @@ -167,6 +204,13 @@ public class RecyclerViewExpandableItemManager { mAdapter = new ExpandableRecyclerViewWrapperAdapter(this, adapter, adapterSavedState); + // move listeners to wrapper adapter + mAdapter.setOnGroupExpandListener(mOnGroupExpandListener); + mOnGroupExpandListener = null; + + mAdapter.setOnGroupCollapseListener(mOnGroupCollapseListener); + mOnGroupCollapseListener = null; + return mAdapter; } @@ -201,7 +245,8 @@ public class RecyclerViewExpandableItemManager { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (handleActionUpOrCancel(rv, e)) { - return true; + // NOTE: it requires to return false to work click effect properly (issue #44) + return false; } break; } @@ -215,7 +260,7 @@ public class RecyclerViewExpandableItemManager { mInitialTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = (int) (e.getY() + 0.5f); - if (holder != null) { + if (holder instanceof ExpandableItemViewHolder) { mTouchedItemId = holder.getItemId(); } else { mTouchedItemId = RecyclerView.NO_ID; @@ -251,13 +296,19 @@ public class RecyclerViewExpandableItemManager { return false; } + final int position = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (position == RecyclerView.NO_POSITION) { + return false; + } + final View view = holder.itemView; final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f); final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f); final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - return mAdapter.onTapItem(holder, viewX, viewY); + return mAdapter.onTapItem(holder, position, viewX, viewY); } /** @@ -265,10 +316,10 @@ public class RecyclerViewExpandableItemManager { * * @param groupPosition The group position to be expanded * - * @return True if the group was expanded,false otherwise (If the group was already expanded, this will return false) + * @return True if the group was expanded, false otherwise (If the group was already expanded, this will return false) */ public boolean expandGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.expandGroup(groupPosition); + return (mAdapter != null) && mAdapter.expandGroup(groupPosition, false); } /** @@ -276,10 +327,10 @@ public class RecyclerViewExpandableItemManager { * * @param groupPosition The group position to be collapsed * - * @return True if the group was collapsed,false otherwise (If the group was already collapsed, this will return false) + * @return True if the group was collapsed, false otherwise (If the group was already collapsed, this will return false) */ public boolean collapseGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.collapseGroup(groupPosition); + return (mAdapter != null) && mAdapter.collapseGroup(groupPosition, false); } /** @@ -328,7 +379,7 @@ public class RecyclerViewExpandableItemManager { * Returns the packed position representation of a child position. * * In general, a packed position should be used in situations where the position given to/returned from - * {@link RecyclerViewExpandableItemManager} method can eirther be a child or group. + * {@link RecyclerViewExpandableItemManager} method can either be a child or group. * The two positions are packed into a single long which can be unpacked using {@link #getPackedPositionChild(long)} and * {@link #getPackedPositionGroup(long)}. * @@ -438,6 +489,303 @@ public class RecyclerViewExpandableItemManager { return ExpandableAdapterHelper.getChildViewType(rawViewType); } + /** + * Register a callback to be invoked when an group item has been expanded. + * + * @param listener The callback that will be invoked. + */ + public void setOnGroupExpandListener(OnGroupExpandListener listener) { + if (mAdapter != null) { + mAdapter.setOnGroupExpandListener(listener); + } else { + // pending + mOnGroupExpandListener = listener; + } + } + + /** + * Register a callback to be invoked when an group item has been collapsed. + * + * @param listener The callback that will be invoked. + */ + public void setOnGroupCollapseListener(OnGroupCollapseListener listener) { + if (mAdapter != null) { + mAdapter.setOnGroupCollapseListener(listener); + } else { + // pending + mOnGroupCollapseListener = listener; + } + } + + /** + * Restore saves state. See {@link #restoreState(android.os.Parcelable, boolean, boolean)}. + * (This method does not invoke any hook methods and listener events) + * + * @param savedState The saved state object + */ + public void restoreState(Parcelable savedState) { + restoreState(savedState, false, false); + } + + /** + * Restore saves state. + * + * This method is useful when the adapter can not be prepared (because data loading may takes time and processed asynchronously) + * before creating this manager instance. + * + * @param savedState The saved state object + * @param callHooks Whether to call hook routines + * ({@link ExpandableItemAdapter#onHookGroupExpand(int, boolean)}, + * {@link ExpandableItemAdapter#onHookGroupCollapse(int, boolean)}) + * @param callListeners Whether to invoke {@link OnGroupExpandListener} and/or {@link OnGroupCollapseListener} listener events + */ + public void restoreState(Parcelable savedState, boolean callHooks, boolean callListeners) { + if (savedState == null) { + return; // do nothing + } + + if (!(savedState instanceof SavedState)) { + throw new IllegalArgumentException("Illegal saved state object passed"); + } + + if (!((mAdapter != null) && (mRecyclerView != null))) { + throw new IllegalStateException("RecyclerView has not been attached"); + } + + mAdapter.restoreState(((SavedState) savedState).adapterSavedState, callHooks, callListeners); + } + + /** + * Notify any registered observers that the group item at groupPosition has changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + *

This method does not notify for children that are contained in the specified group. + * If children have also changed, use {@link #notifyGroupAndChildrenItemsChanged(int)} instead.

+ * + * @param groupPosition Position of the group item that has changed + * + * @see #notifyGroupAndChildrenItemsChanged(int) + */ + public void notifyGroupItemChanged(int groupPosition) { + mAdapter.notifyGroupItemChanged(groupPosition); + } + + /** + * Notify any registered observers that the group and children items at groupPosition have changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + * @param groupPosition Position of the group item which contains changed children + * + * @see #notifyGroupItemChanged(int) + * @see #notifyChildrenOfGroupItemChanged(int) + */ + public void notifyGroupAndChildrenItemsChanged(int groupPosition) { + mAdapter.notifyGroupAndChildrenItemsChanged(groupPosition); + } + + /** + * Notify any registered observers that the children items contained in the group item at groupPosition have changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + *

This method does not notify for the group item. + * If the group has also changed, use {@link #notifyGroupAndChildrenItemsChanged(int)} instead.

+ * + * @param groupPosition Position of the group item which contains changed children + * + * @see #notifyGroupAndChildrenItemsChanged(int) + */ + public void notifyChildrenOfGroupItemChanged(int groupPosition) { + mAdapter.notifyChildrenOfGroupItemChanged(groupPosition); + } + + /** + * Notify any registered observers that the child item at {groupPosition, childPosition} has changed. + * + *

This is an item change event, not a structural change event. It indicates that any + * reflection of the data at {groupPosition, childPosition} is out of date and should be updated. + * The item at {groupPosition, childPosition} retains the same identity.

+ * + * @param groupPosition Position of the group item which contains the changed child + * @param childPosition Position of the child item in the group that has changed + * + * @see #notifyChildItemRangeChanged(int, int, int) + */ + public void notifyChildItemChanged(int groupPosition, int childPosition) { + mAdapter.notifyChildItemChanged(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the itemCount child items starting at + * position {groupPosition, childPosition} have changed. + * + *

This is an item change event, not a structural change event. It indicates that + * any reflection of the data in the given position range is out of date and should + * be updated. The items in the given range retain the same identity.

+ * + * @param groupPosition Position of the group item which contains the changed child + * @param childPositionStart Position of the first child item in the group that has changed + * @param itemCount Number of items that have changed + * + * @see #notifyChildItemChanged(int, int) + */ + public void notifyChildItemRangeChanged(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeChanged(groupPosition, childPositionStart, itemCount); + } + + /** + * Notify any registered observers that the group item reflected at groupPosition + * has been newly inserted. The group item previously at groupPosition is now at + * position groupPosition + 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param groupPosition Position of the newly inserted group item in the data set + * + * @see #notifyGroupItemRangeInserted(int, int) + */ + public void notifyGroupItemInserted(int groupPosition) { + mAdapter.notifyGroupItemInserted(groupPosition); + } + + /** + * Notify any registered observers that the currently reflected itemCount + * group items starting at groupPositionStart have been newly inserted. The group items + * previously located at groupPositionStart and beyond can now be found starting + * at position groupPositionStart + itemCount. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPositionStart Position of the first group item that was inserted + * @param itemCount Number of group items inserted + * + * @see #notifyGroupItemInserted(int) + */ + public void notifyGroupItemRangeInserted(int groupPositionStart, int itemCount) { + mAdapter.notifyGroupItemRangeInserted(groupPositionStart, itemCount); + } + + + /** + * Notify any registered observers that the group item reflected at groupPosition + * has been newly inserted. The group item previously at groupPosition is now at + * position groupPosition + 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param groupPosition Position of the group item which contains the inserted child + * @param childPosition Position of the newly inserted child item in the data set + * + * @see #notifyChildItemRangeInserted(int, int, int) + */ + public void notifyChildItemInserted(int groupPosition, int childPosition) { + mAdapter.notifyChildItemInserted(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the currently reflected itemCount + * child items starting at childPositionStart have been newly inserted. The child items + * previously located at childPositionStart and beyond can now be found starting + * at position childPositionStart + itemCount. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which contains the inserted child + * @param childPositionStart Position of the first child item that was inserted + * @param itemCount Number of child items inserted + * + * @see #notifyChildItemInserted(int, int) + */ + public void notifyChildItemRangeInserted(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeInserted(groupPosition, childPositionStart, itemCount); + } + + /** + * Notify any registered observers that the group item previously located at groupPosition + * has been removed from the data set. The group items previously located at and after + * groupPosition may now be found at oldGroupPosition - 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item that has now been removed + * + * @see #notifyGroupItemRangeRemoved(int, int) + */ + public void notifyGroupItemRemoved(int groupPosition) { + mAdapter.notifyGroupItemRemoved(groupPosition); + } + + /** + * Notify any registered observers that the itemCount group items previously + * located at groupPositionStart have been removed from the data set. The group items + * previously located at and after groupPositionStart + itemCount may now be found + * at oldPosition - itemCount. + * + *

This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPositionStart Previous position of the first group item that was removed + * @param itemCount Number of group items removed from the data set + */ + public void notifyGroupItemRangeRemoved(int groupPositionStart, int itemCount) { + mAdapter.notifyGroupItemRangeRemoved(groupPositionStart, itemCount); + } + + /** + * Notify any registered observers that the child item previously located at childPosition + * has been removed from the data set. The child items previously located at and after + * childPosition may now be found at oldGroupPosition - 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which was the parent of the child item that was removed + * @param childPosition Position of the child item that has now been removed + * + * @see #notifyGroupItemRangeRemoved(int, int) + */ + public void notifyChildItemRemoved(int groupPosition, int childPosition) { + mAdapter.notifyChildItemRemoved(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the itemCount child items previously + * located at childPositionStart have been removed from the data set. The child items + * previously located at and after childPositionStart + itemCount may now be found + * at oldPosition - itemCount. + * + *

This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which was the parent of the child item that was removed + * @param childPositionStart Previous position of the first child item that was removed + * @param itemCount Number of child items removed from the data set + */ + public void notifyChildItemRangeRemoved(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeRemoved(groupPosition, childPositionStart, itemCount); + } + public static class SavedState implements Parcelable { final int [] adapterSavedState; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java old mode 100644 new mode 100755 index 925a006146c..7ccd5b54fdd --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java @@ -32,6 +32,7 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -41,40 +42,62 @@ public class ItemSlidingAnimator { private Interpolator mSlideToDefaultPositionAnimationInterpolator = new AccelerateDecelerateInterpolator(); private Interpolator mSlideToOutsideOfWindowAnimationInterpolator = new AccelerateInterpolator(0.8f); private List mActive; + private List> mDeferredProcesses; private int[] mTmpLocation = new int[2]; private Rect mTmpRect = new Rect(); private int mImmediatelySetTranslationThreshold; public ItemSlidingAnimator() { mActive = new ArrayList<>(); + mDeferredProcesses = new ArrayList<>(); } public void slideToDefaultPosition(RecyclerView.ViewHolder holder, boolean shouldAnimate, long duration) { + cancelDeferredProcess(holder); slideToSpecifiedPositionInternal(holder, 0, shouldAnimate, duration); } public void slideToOutsideOfWindow(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { + cancelDeferredProcess(holder); slideToOutsideOfWindowInternal(holder, toLeft, shouldAnimate, duration); } public void slideToSpecifiedPosition(RecyclerView.ViewHolder holder, float position) { + cancelDeferredProcess(holder); slideToSpecifiedPositionInternal(holder, position, false, 0); } - private boolean slideToSpecifiedPositionInternal(RecyclerView.ViewHolder holder, float position, boolean shouldAnimate, long duration) { - final int translationX; + private void cancelDeferredProcess(RecyclerView.ViewHolder holder) { + int n = mDeferredProcesses.size(); + for (int i = n - 1; i >= 0; i--) { + ViewHolderDeferredProcess dp = mDeferredProcesses.get(i).get(); + if (dp == null || dp.hasTargetViewHolderOrLostReference(holder)) { + mDeferredProcesses.remove(i); + } + } + } + + private boolean slideToSpecifiedPositionInternal(final RecyclerView.ViewHolder holder, final float position, boolean shouldAnimate, long duration) { duration = (shouldAnimate) ? duration : 0; if (position != 0.0f) { final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + final int width = containerView.getWidth(); - translationX = (int) (containerView.getWidth() * position + 0.5f); + if (width != 0) { + final int translationX; + translationX = (int) (width * position + 0.5f); + return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator); + } else { + final DeferredSlideProcess deferredProcess = new DeferredSlideProcess(holder, position); + mDeferredProcesses.add(new WeakReference(deferredProcess)); + containerView.post(deferredProcess); + return false; + } } else { - translationX = 0; + return animateSlideInternalCompat(holder, 0, duration, mSlideToDefaultPositionAnimationInterpolator); } - - return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator); } private boolean slideToOutsideOfWindowInternal(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { @@ -124,12 +147,20 @@ public class ItemSlidingAnimator { if (supportsViewPropertyAnimator()) { return animateSlideInternal(holder, translationX, duration, interpolator); } else { - return animateSlideInternalPreHoneycomb(holder, translationX); + return slideInternalPreHoneycomb(holder, translationX); + } + } + + static void slideInternalCompat(RecyclerView.ViewHolder holder, int translationX) { + if (supportsViewPropertyAnimator()) { + slideInternal(holder, translationX); + } else { + slideInternalPreHoneycomb(holder, translationX); } } @SuppressLint("RtlHardcoded") - private boolean animateSlideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) { + private static boolean slideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) { if (!(holder instanceof SwipeableItemViewHolder)) { return false; } @@ -153,6 +184,29 @@ public class ItemSlidingAnimator { return false; } + private static int getTranslationXPreHoneycomb(RecyclerView.ViewHolder holder) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + + final ViewGroup.LayoutParams lp = containerView.getLayoutParams(); + if (lp instanceof ViewGroup.MarginLayoutParams) { + final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; + return mlp.leftMargin; + } else { + Log.w(TAG, "should use MarginLayoutParams supported view for compatibility on Android 2.3"); + return 0; + } + } + + private static void slideInternal(final RecyclerView.ViewHolder holder, int translationX) { + if (!(holder instanceof SwipeableItemViewHolder)) { + return; + } + + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + ViewCompat.animate(containerView).cancel(); + ViewCompat.setTranslationX(containerView, translationX); + } + private boolean animateSlideInternal(final RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) { if (!(holder instanceof SwipeableItemViewHolder)) { return false; @@ -164,8 +218,13 @@ public class ItemSlidingAnimator { endAnimation(holder); + final int curTranslationX = (int) (ViewCompat.getTranslationX(containerView) + 0.5f); final int toX = translationX; + if (curTranslationX == toX) { + return false; + } + if (duration == 0 || Math.abs(toX - prevTranslationX) <= mImmediatelySetTranslationThreshold) { ViewCompat.setTranslationX(containerView, toX); return false; @@ -209,6 +268,8 @@ public class ItemSlidingAnimator { return; } + cancelDeferredProcess(holder); + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); ViewCompat.animate(containerView).cancel(); @@ -244,4 +305,56 @@ public class ItemSlidingAnimator { private static boolean supportsViewPropertyAnimator() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } + + public int getSwipeContainerViewTranslationX(RecyclerView.ViewHolder holder) { + if (supportsViewPropertyAnimator()) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + return (int) (ViewCompat.getTranslationX(containerView) + 0.5f); + } else { + return getTranslationXPreHoneycomb(holder); + } + } + + private static abstract class ViewHolderDeferredProcess implements Runnable { + final WeakReference mRefHolder; + + public ViewHolderDeferredProcess(RecyclerView.ViewHolder holder) { + mRefHolder = new WeakReference(holder); + } + + @Override + public void run() { + RecyclerView.ViewHolder holder = mRefHolder.get(); + + if (holder != null) { + onProcess(holder); + } + } + + public boolean hasTargetViewHolderOrLostReference(RecyclerView.ViewHolder holder) { + RecyclerView.ViewHolder holder2 = mRefHolder.get(); + return ((holder2 == null) || (holder2 == holder)); + } + + protected abstract void onProcess(RecyclerView.ViewHolder holder); + } + + private static final class DeferredSlideProcess extends ViewHolderDeferredProcess { + final float mPosition; + + public DeferredSlideProcess(RecyclerView.ViewHolder holder, float position) { + super(holder); + mPosition = position; + } + + @Override + protected void onProcess(RecyclerView.ViewHolder holder) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + final int width = containerView.getWidth(); + final int translationX; + + translationX = (int) (width * mPosition + 0.5f); + slideInternalCompat(holder, translationX); + } + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java old mode 100644 new mode 100755 index 810f98835ee..648bc7b7302 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java @@ -40,7 +40,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * None. (internal default value, this value is not used for the argument) */ @@ -48,7 +48,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Canceled. */ @@ -56,7 +56,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Swipe left performed. */ @@ -64,7 +64,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Swipe right performed. */ @@ -73,7 +73,7 @@ public class RecyclerViewSwipeManager { // --- /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe left" (completely no reactions) */ @@ -81,7 +81,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe left" (not swipeable, but rubber-band effect applied) */ @@ -89,7 +89,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can swipe left" */ @@ -97,7 +97,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe right" (completely no reactions) */ @@ -105,7 +105,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe right" (not swipeable, but rubber-band effect applied) */ @@ -113,7 +113,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can swipe right" */ @@ -141,7 +141,7 @@ public class RecyclerViewSwipeManager { // --- /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the neutral (= not swiping) item. */ @@ -149,7 +149,7 @@ public class RecyclerViewSwipeManager { /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the swiping-left item. */ @@ -157,7 +157,7 @@ public class RecyclerViewSwipeManager { /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the swiping-right item. */ @@ -217,6 +217,29 @@ public class RecyclerViewSwipeManager { // --- + /** + * Used for listening item swipe events + */ + public interface OnItemSwipeEventListener { + /** + * Callback method to be invoked when swiping is started. + * + * @param position The position of the item. + */ + void onItemSwipeStarted(int position); + + /** + * Callback method to be invoked when swiping is finished. + * + * @param position The position of the item. + * @param result The result code of the swipe operation. + * @param afterSwipeReaction The reaction type to the swipe operation. + */ + void onItemSwipeFinished(int position, int result, int afterSwipeReaction); + } + + // --- + private static final int MIN_DISTANCE_TOUCH_SLOP_MUL = 10; private static final int SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP = 8; @@ -239,12 +262,14 @@ public class RecyclerViewSwipeManager { private ItemSlidingAnimator mItemSlideAnimator; private SwipeableItemWrapperAdapter mAdapter; private RecyclerView.ViewHolder mSwipingItem; + private int mSwipingItemPosition = RecyclerView.NO_POSITION; private Rect mSwipingItemMargins = new Rect(); private int mTouchedItemOffsetX; private int mLastTouchX; private int mSwipingItemReactionType; private VelocityTracker mVelocityTracker; private SwipingItemOperator mSwipingItemOperator; + private OnItemSwipeEventListener mItemSwipeEventListener; /** * Constructor. @@ -260,6 +285,10 @@ public class RecyclerViewSwipeManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewSwipeManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; mItemSlideAnimator = new ItemSlidingAnimator(); mVelocityTracker = VelocityTracker.obtain(); @@ -437,7 +466,7 @@ public class RecyclerViewSwipeManager { return false; } - final int itemPosition = holder.getPosition(); + final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder); // verify the touched item is valid state if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) { @@ -456,7 +485,7 @@ public class RecyclerViewSwipeManager { final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - final int reactionType = mAdapter.getSwipeReactionType(holder, viewX, viewY); + final int reactionType = mAdapter.getSwipeReactionType(holder, itemPosition, viewX, viewY); if (reactionType == 0) { return false; @@ -538,11 +567,17 @@ public class RecyclerViewSwipeManager { return false; } + final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (itemPosition == RecyclerView.NO_POSITION) { + return false; + } + if (LOCAL_LOGD) { Log.d(TAG, "swiping started"); } - startSwiping(rv, e, holder); + startSwiping(rv, e, holder, itemPosition); return true; } @@ -556,13 +591,14 @@ public class RecyclerViewSwipeManager { mSwipingItemOperator.update(swipeDistance); } - private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder) { + private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, int itemPosition) { mSwipingItem = holder; + mSwipingItemPosition = itemPosition; mLastTouchX = (int) (e.getX() + 0.5f); mTouchedItemOffsetX = mLastTouchX; CustomRecyclerViewUtils.getLayoutMargins(holder.itemView, mSwipingItemMargins); - mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemReactionType); + mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemPosition, mSwipingItemReactionType); mSwipingItemOperator.start(); mVelocityTracker.clear(); @@ -570,8 +606,13 @@ public class RecyclerViewSwipeManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); + // raise onItemSwipeStarted() event + if (mItemSwipeEventListener != null) { + mItemSwipeEventListener.onItemSwipeStarted(itemPosition); + } + // raise onSwipeItemStarted() event - mAdapter.onSwipeItemStarted(this, holder); + mAdapter.onSwipeItemStarted(this, holder, itemPosition); } private void finishSwiping(int result) { @@ -585,9 +626,12 @@ public class RecyclerViewSwipeManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); } + final int itemPosition = mSwipingItemPosition; + mVelocityTracker.clear(); mSwipingItem = null; + mSwipingItemPosition = RecyclerView.NO_POSITION; mLastTouchX = 0; mInitialTouchX = 0; mTouchedItemOffsetX = 0; @@ -603,7 +647,7 @@ public class RecyclerViewSwipeManager { int afterReaction = AFTER_SWIPE_REACTION_DEFAULT; if (mAdapter != null) { - afterReaction = mAdapter.onSwipeItemFinished(swipingItem, result); + afterReaction = mAdapter.onSwipeItemFinished(swipingItem, itemPosition, result); } afterReaction = correctAfterReaction(result, afterReaction); @@ -640,7 +684,12 @@ public class RecyclerViewSwipeManager { } if (mAdapter != null) { - mAdapter.onSwipeItemFinished2(swipingItem, result, afterReaction); + mAdapter.onSwipeItemFinished2(swipingItem, itemPosition, result, afterReaction); + } + + // raise onItemSwipeFinished() event + if (mItemSwipeEventListener != null) { + mItemSwipeEventListener.onItemSwipeFinished(itemPosition, result, afterReaction); } } @@ -711,7 +760,24 @@ public class RecyclerViewSwipeManager { mMoveToOutsideWindowAnimationDuration = duration; } - /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, float prevAmount, float amount, boolean shouldAnimate) { + /** + * Gets OnItemSwipeEventListener listener + * @return The listener object + */ + public OnItemSwipeEventListener getOnItemSwipeEventListener() { + return mItemSwipeEventListener; + } + + /** + * Sets OnItemSwipeEventListener listener + * + * @param listener The listener object + */ + public void setOnItemSwipeEventListener(OnItemSwipeEventListener listener) { + mItemSwipeEventListener = listener; + } + + /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, int itemPosition, float prevAmount, float amount, boolean shouldAnimate) { final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder; final View containerView = holder2.getSwipeableContainerView(); @@ -737,13 +803,28 @@ public class RecyclerViewSwipeManager { if (amount == 0.0f) { slideItem(holder, amount, shouldAnimate); - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); + mAdapter.setSwipeBackgroundDrawable(holder, itemPosition, reqBackgroundType); } else { - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); - slideItem(holder, amount, shouldAnimate); + float adjustedAmount = amount; + + adjustedAmount = Math.max(adjustedAmount, holder2.getMaxLeftSwipeAmount()); + adjustedAmount = Math.min(adjustedAmount, holder2.getMaxRightSwipeAmount()); + + mAdapter.setSwipeBackgroundDrawable(holder, itemPosition, reqBackgroundType); + slideItem(holder, adjustedAmount, shouldAnimate); } } + /*package*/ void cancelPendingAnimations(RecyclerView.ViewHolder holder) { + if (mItemSlideAnimator != null) { + mItemSlideAnimator.endAnimation(holder); + } + } + + /*package*/ int getSwipeContainerViewTranslationX(RecyclerView.ViewHolder holder) { + return mItemSlideAnimator.getSwipeContainerViewTranslationX(holder); + } + private static boolean supportsViewPropertyAnimator() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java old mode 100644 new mode 100755 index 8065322bf76..7b949f6ff5d --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java @@ -24,6 +24,7 @@ public interface SwipeableItemAdapter { * Called when user is attempt to swipe the item. * * @param holder The ViewHolder which is associated to item user is attempt to start swiping. + * @param position The position of the item within the adapter's data set. * @param x Touched X position. Relative from the itemView's top-left. * @param y Touched Y position. Relative from the itemView's top-left. @@ -35,18 +36,19 @@ public interface SwipeableItemAdapter { * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT} * - {@link RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT} */ - int onGetSwipeReactionType(T holder, int x, int y); + int onGetSwipeReactionType(T holder, int position, int x, int y); /** * Called when sets background of the swiping item. * * @param holder The ViewHolder which is associated to the swiping item. + * @param position The position of the item within the adapter's data set. * @param type Background type. One of the * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND}, * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}. */ - void onSetSwipeBackground(T holder, int type); + void onSetSwipeBackground(T holder, int position, int type); /** * Called when item is swiped. @@ -54,6 +56,7 @@ public interface SwipeableItemAdapter { * *Note that do not change data set and do not call notifyDataXXX() methods inside of this method.* * * @param holder The ViewHolder which is associated to the swiped item. + * @param position The position of the item within the adapter's data set. * @param result The result code of user's swipe operation. * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or @@ -64,14 +67,15 @@ public interface SwipeableItemAdapter { * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. */ - int onSwipeItem(T holder, int result); + int onSwipeItem(T holder, int position, int result); /** - * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * You can update data set and call notifyDataXXX() methods inside of this method. * * @param holder The ViewHolder which is associated to the swiped item. + * @param position The position of the item within the adapter's data set. * @param result The result code of user's swipe operation. * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or @@ -80,5 +84,5 @@ public interface SwipeableItemAdapter { * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. */ - void onPerformAfterSwipeReaction(T holder, int result, int reaction); + void onPerformAfterSwipeReaction(T holder, int position, int result, int reaction); } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java old mode 100644 new mode 100755 index 5c4714364dc..086a24e7045 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java @@ -104,6 +104,34 @@ public interface SwipeableItemViewHolder { */ float getSwipeItemSlideAmount(); + /** + * Sets the maximum item right swipe amount. + * + * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + void setMaxLeftSwipeAmount(float amount); + + /** + * Gets the maximum item left swipe amount. + * + * @return Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + float getMaxLeftSwipeAmount(); + + /** + * Sets the maximum item right swipe amount. + * + * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + void setMaxRightSwipeAmount(float amount); + + /** + * Gets the maximum item right swipe amount. + * + * @return Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + float getMaxRightSwipeAmount(); + /** * Gets the container view for the swipeable area. * diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java old mode 100644 new mode 100755 index 6cc6ee9607a..0dba924866e --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java @@ -67,6 +67,8 @@ class SwipeableItemWrapperAdapter extends Ba // reset SwipeableItemViewHolder state if (holder instanceof SwipeableItemViewHolder) { + mSwipeManager.cancelPendingAnimations(holder); + ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(0.0f); View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); @@ -116,7 +118,7 @@ class SwipeableItemWrapperAdapter extends Ba if ((prevSwipeItemSlideAmount != swipeItemSlideAmount) || !(mSwipeManager.isSwiping() || mSwipeManager.isAnimationRunning(holder))) { - mSwipeManager.applySlideItem(holder, prevSwipeItemSlideAmount, swipeItemSlideAmount, true); + mSwipeManager.applySlideItem(holder, position, prevSwipeItemSlideAmount, swipeItemSlideAmount, true); } } } @@ -175,32 +177,32 @@ class SwipeableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - int getSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) { + int getSwipeReactionType(RecyclerView.ViewHolder holder, int position, int x, int y) { if (LOCAL_LOGV) { - Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", x = " + x + ", y = " + y + ")"); + Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", position = " + position + ", x = " + x + ", y = " + y + ")"); } - return mSwipeableItemAdapter.onGetSwipeReactionType(holder, x, y); + return mSwipeableItemAdapter.onGetSwipeReactionType(holder, position, x, y); } // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int type) { + void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int position, int type) { if (LOCAL_LOGV) { - Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", type = " + type + ")"); + Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", position = " + position + ", type = " + type + ")"); } - mSwipeableItemAdapter.onSetSwipeBackground(holder, type); + mSwipeableItemAdapter.onSetSwipeBackground(holder, position, type); } // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder) { + /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder, int position) { if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ")"); + Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ", position = " + position + ")"); } - mSwipingItemPosition = holder.getPosition(); + mSwipingItemPosition = position; notifyDataSetChanged(); } @@ -208,25 +210,25 @@ class SwipeableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - int onSwipeItemFinished(RecyclerView.ViewHolder holder, int result) { + int onSwipeItemFinished(RecyclerView.ViewHolder holder, int position, int result) { if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", result = " + result + ")"); + Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", position = " + position + ", result = " + result + ")"); } mSwipingItemPosition = RecyclerView.NO_POSITION; - return mSwipeableItemAdapter.onSwipeItem(holder, result); + return mSwipeableItemAdapter.onSwipeItem(holder, position, result); } /*package*/ @SuppressWarnings("unchecked") - void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int result, int afterReaction) { + void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int position, int result, int afterReaction) { ((SwipeableItemViewHolder) holder).setSwipeResult(result); ((SwipeableItemViewHolder) holder).setAfterSwipeReaction(afterReaction); ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(getSwipeAmountFromAfterReaction(result, afterReaction)); - mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, result, afterReaction); + mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, position, result, afterReaction); notifyDataSetChanged(); } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java old mode 100644 new mode 100755 index c735d436266..29f05c1e2e6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java @@ -29,11 +29,13 @@ class SwipingItemOperator { private static final int REACTION_CAN_SWIPE = SwipeReactionUtils.REACTION_CAN_SWIPE; private static final float RUBBER_BAND_LIMIT = 0.15f; + private static final int MIN_GRABBING_AREA_WIDTH = 48; private static final Interpolator RUBBER_BAND_INTERPOLATOR = new RubberBandInterpolator(RUBBER_BAND_LIMIT); private RecyclerViewSwipeManager mSwipeManager; private RecyclerView.ViewHolder mSwipingItem; + private int mSwipingItemPosition; private View mSwipingItemContainerView; private int mLeftSwipeReactionType; private int mRightSwipeReactionType; @@ -41,10 +43,12 @@ class SwipingItemOperator { private float mInvSwipingItemWidth; private int mSwipeDistance; private float mPrevTranslateAmount; + private int mInitialTranslateAmount; - public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int swipeReactionType) { + public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int position, int swipeReactionType) { mSwipeManager = manager; mSwipingItem = swipingItem; + mSwipingItemPosition = position; mLeftSwipeReactionType = SwipeReactionUtils.extractLeftReaction(swipeReactionType); mRightSwipeReactionType = SwipeReactionUtils.extractRightReaction(swipeReactionType); @@ -53,9 +57,13 @@ class SwipingItemOperator { mInvSwipingItemWidth = (mSwipingItemWidth != 0) ? (1.0f / mSwipingItemWidth) : 0.0f; } - @SuppressWarnings("EmptyMethod") public void start() { - // nothing to do here. + float density = mSwipingItem.itemView.getResources().getDisplayMetrics().density; + int maxAmount = Math.max(0, mSwipingItemWidth - (int) (density * MIN_GRABBING_AREA_WIDTH)); + + mInitialTranslateAmount = mSwipeManager.getSwipeContainerViewTranslationX(mSwipingItem); + mInitialTranslateAmount = Math.min(mInitialTranslateAmount, maxAmount); + mInitialTranslateAmount = Math.max(mInitialTranslateAmount, -maxAmount); } public void finish() { @@ -67,6 +75,7 @@ class SwipingItemOperator { mLeftSwipeReactionType = REACTION_CAN_NOT_SWIPE; mRightSwipeReactionType = REACTION_CAN_NOT_SWIPE; mPrevTranslateAmount = 0; + mInitialTranslateAmount = 0; mSwipingItemContainerView = null; } @@ -77,7 +86,9 @@ class SwipingItemOperator { mSwipeDistance = swipeDistance; - final int reactionType = (mSwipeDistance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType; + int distance = mSwipeDistance + mInitialTranslateAmount; + + final int reactionType = (distance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType; float translateAmount = 0; @@ -85,15 +96,15 @@ class SwipingItemOperator { case REACTION_CAN_NOT_SWIPE: break; case REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT: - float proportion = Math.min(Math.abs(mSwipeDistance), mSwipingItemWidth) * mInvSwipingItemWidth; - translateAmount = Math.signum(mSwipeDistance) * RUBBER_BAND_INTERPOLATOR.getInterpolation(proportion); + float proportion = Math.min(Math.abs(distance), mSwipingItemWidth) * mInvSwipingItemWidth; + translateAmount = Math.signum(distance) * RUBBER_BAND_INTERPOLATOR.getInterpolation(proportion); break; case REACTION_CAN_SWIPE: - translateAmount = Math.min(Math.max((mSwipeDistance * mInvSwipingItemWidth), -1.0f), 1.0f); + translateAmount = Math.min(Math.max((distance * mInvSwipingItemWidth), -1.0f), 1.0f); break; } - mSwipeManager.applySlideItem(mSwipingItem, mPrevTranslateAmount, translateAmount, false); + mSwipeManager.applySlideItem(mSwipingItem, mSwipingItemPosition, mPrevTranslateAmount, translateAmount, false); mPrevTranslateAmount = translateAmount; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java old mode 100644 new mode 100755 index 3f8003af242..5d2e27fe4cf --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java @@ -22,6 +22,9 @@ import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; +/** + * Hooks touch events to avoid unexpected scrolling. + */ public class RecyclerViewTouchActionGuardManager { private static final String TAG = "ARVTouchActionGuardMgr"; @@ -37,6 +40,9 @@ public class RecyclerViewTouchActionGuardManager { private boolean mEnabled; private boolean mInterceptScrollingWhileAnimationRunning; + /** + * Constructor. + */ public RecyclerViewTouchActionGuardManager() { mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override @@ -48,13 +54,27 @@ public class RecyclerViewTouchActionGuardManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewTouchActionGuardManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; } + /** + * Indicates this manager instance has released or not. + * + * @return True if this manager instance has released + */ public boolean isReleased() { return (mInternalUseOnItemTouchListener == null); } + /** + * Attaches {@link android.support.v7.widget.RecyclerView} instance. + * + * @param rv The {@link android.support.v7.widget.RecyclerView} instance + */ public void attachRecyclerView(RecyclerView rv) { if (rv == null) { throw new IllegalArgumentException("RecyclerView cannot be null"); @@ -74,6 +94,11 @@ public class RecyclerViewTouchActionGuardManager { mTouchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop(); } + /** + * Detach the {@link android.support.v7.widget.RecyclerView} instance and release internal field references. + * + * This method should be called in order to avoid memory leaks. + */ public void release() { if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) { mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); @@ -163,6 +188,11 @@ public class RecyclerViewTouchActionGuardManager { mGuarding = false; } + /** + * Sets whether to use touch guard feature. If set false, all touch event interceptions will be disabled. + * + * @param enabled enabled / disabled + */ public void setEnabled(boolean enabled) { if (mEnabled == enabled) { return; @@ -174,14 +204,29 @@ public class RecyclerViewTouchActionGuardManager { } } + /** + * Checks whether the touch guard feature is enabled. + * + * @return True for currently touch guard feature is enabled, otherwise false + */ public boolean isEnabled() { return mEnabled; } - public void setInterceptVerticalScrollingWhileAnimationRunning(boolean value) { - mInterceptScrollingWhileAnimationRunning = value; + /** + * Sets whether to use interception of "vertical scroll while animation running". + * + * @param enabled enabled / disabled + */ + public void setInterceptVerticalScrollingWhileAnimationRunning(boolean enabled) { + mInterceptScrollingWhileAnimationRunning = enabled; } + /** + * Checks whether the interception of "vertical scroll while animation running" is enabled. + * + * @return enabled / disabled + */ public boolean isInterceptScrollingWhileAnimationRunning() { return mInterceptScrollingWhileAnimationRunning; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java old mode 100644 new mode 100755 index 3e8dc254a1a..b9d0c9577a1 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java @@ -29,6 +29,8 @@ public abstract class AbstractDraggableSwipeableItemViewHolder extends RecyclerV private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE; private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; private float mSwipeAmount; + private float mMaxLeftSwipeAmount = RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_LEFT; + private float mMaxRightSwipeAmount = RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_RIGHT; public AbstractDraggableSwipeableItemViewHolder(View itemView) { super(itemView); @@ -84,4 +86,23 @@ public abstract class AbstractDraggableSwipeableItemViewHolder extends RecyclerV mSwipeAmount = amount; } + @Override + public void setMaxLeftSwipeAmount(float amount) { + mMaxLeftSwipeAmount = amount; + } + + @Override + public float getMaxLeftSwipeAmount() { + return mMaxLeftSwipeAmount; + } + + @Override + public void setMaxRightSwipeAmount(float amount) { + mMaxRightSwipeAmount = amount; + } + + @Override + public float getMaxRightSwipeAmount() { + return mMaxRightSwipeAmount; + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java old mode 100644 new mode 100755 index 6fb94598a9d..59f0bbc59f2 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java @@ -85,4 +85,22 @@ public abstract class AbstractExpandableItemAdapter + + + + + + + + + diff --git a/mobile/google-android-support-v13/.gitignore b/mobile/google-android-support-v13/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/mobile/google-android-support-v13/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/mobile/google-android-support-v13/.project b/mobile/google-android-support-v13/.project new file mode 100644 index 00000000000..bedbd244bfd --- /dev/null +++ b/mobile/google-android-support-v13/.project @@ -0,0 +1,33 @@ + + + google-android-support-v13 + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs b/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..303053a514f --- /dev/null +++ b/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=ignore +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml b/mobile/google-android-support-v13/AndroidManifest.xml similarity index 72% rename from mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml rename to mobile/google-android-support-v13/AndroidManifest.xml index bb784b76b9d..dc6e6c359e3 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml +++ b/mobile/google-android-support-v13/AndroidManifest.xml @@ -1,5 +1,6 @@ - + - + - - 0px + - + \ No newline at end of file diff --git a/mobile/google-android-support-v13/project.properties b/mobile/google-android-support-v13/project.properties new file mode 100644 index 00000000000..362a0a3082a --- /dev/null +++ b/mobile/google-android-support-v13/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-22 +android.library=true diff --git a/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF b/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java new file mode 100644 index 00000000000..eec3ea50007 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.v13.app; + +import android.app.Fragment; + +/** + * Helper for accessing features in {@link Fragment} introduced after + * API level 13 in a backwards compatible fashion. + */ +public class FragmentCompat { + interface FragmentCompatImpl { + void setMenuVisibility(Fragment f, boolean visible); + void setUserVisibleHint(Fragment f, boolean deferStart); + } + + static class BaseFragmentCompatImpl implements FragmentCompatImpl { + public void setMenuVisibility(Fragment f, boolean visible) { + } + public void setUserVisibleHint(Fragment f, boolean deferStart) { + } + } + + static class ICSFragmentCompatImpl extends BaseFragmentCompatImpl { + @Override + public void setMenuVisibility(Fragment f, boolean visible) { + FragmentCompatICS.setMenuVisibility(f, visible); + } + } + + static class ICSMR1FragmentCompatImpl extends ICSFragmentCompatImpl { + @Override + public void setUserVisibleHint(Fragment f, boolean deferStart) { + FragmentCompatICSMR1.setUserVisibleHint(f, deferStart); + } + } + + static final FragmentCompatImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 15) { + IMPL = new ICSMR1FragmentCompatImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new ICSFragmentCompatImpl(); + } else { + IMPL = new BaseFragmentCompatImpl(); + } + } + + /** + * Call {@link Fragment#setMenuVisibility(boolean) Fragment.setMenuVisibility(boolean)} + * if running on an appropriate version of the platform. + */ + public static void setMenuVisibility(Fragment f, boolean visible) { + IMPL.setMenuVisibility(f, visible); + } + + /** + * Call {@link Fragment#setUserVisibleHint(boolean) setUserVisibleHint(boolean)} + * if running on an appropriate version of the platform. + */ + public static void setUserVisibleHint(Fragment f, boolean deferStart) { + IMPL.setUserVisibleHint(f, deferStart); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java new file mode 100644 index 00000000000..1beb704db81 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.v13.app; + +import android.app.Fragment; + +class FragmentCompatICS { + public static void setMenuVisibility(Fragment f, boolean visible) { + f.setMenuVisibility(visible); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java new file mode 100644 index 00000000000..39f09229b88 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.v13.app; + +import android.app.Fragment; + +class FragmentCompatICSMR1 { + public static void setUserVisibleHint(Fragment f, boolean isVisible) { + f.setUserVisibleHint(isVisible); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java new file mode 100644 index 00000000000..60aa5d0e747 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.v13.app; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.os.Parcelable; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * represents each page as a {@link android.app.Fragment} that is persistently + * kept in the fragment manager as long as the user can return to the page. + * + *

This version of the pager is best for use when there are a handful of + * typically more static fragments to be paged through, such as a set of tabs. + * The fragment of each page the user visits will be kept in memory, though its + * view hierarchy may be destroyed when not visible. This can result in using + * a significant amount of memory since fragment instances can hold on to an + * arbitrary amount of state. For larger sets of pages, consider + * {@link FragmentStatePagerAdapter}. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentPagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentPagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentPagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + private Fragment mCurrentPrimaryItem = null; + + public FragmentPagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + final long itemId = getItemId(position); + + // Do we already have this fragment? + String name = makeFragmentName(container.getId(), itemId); + Fragment fragment = mFragmentManager.findFragmentByTag(name); + if (fragment != null) { + if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); + mCurTransaction.attach(fragment); + } else { + fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); + mCurTransaction.add(container.getId(), fragment, + makeFragmentName(container.getId(), itemId)); + } + if (fragment != mCurrentPrimaryItem) { + FragmentCompat.setMenuVisibility(fragment, false); + FragmentCompat.setUserVisibleHint(fragment, false); + } + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + + " v=" + ((Fragment)object).getView()); + mCurTransaction.detach((Fragment)object); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false); + FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false); + } + if (fragment != null) { + FragmentCompat.setMenuVisibility(fragment, true); + FragmentCompat.setUserVisibleHint(fragment, true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + return null; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Return a unique identifier for the item at the given position. + * + *

The default implementation returns the given position. + * Subclasses should override this method if the positions of items can change.

+ * + * @param position Position within this adapter + * @return Unique identifier for the item at position + */ + public long getItemId(int position) { + return position; + } + + private static String makeFragmentName(int viewId, long id) { + return "android:switcher:" + viewId + ":" + id; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java new file mode 100644 index 00000000000..89be82e8e54 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.v13.app; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + *

This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStatePagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + FragmentCompat.setMenuVisibility(fragment, false); + FragmentCompat.setUserVisibleHint(fragment, false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false); + FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false); + } + if (fragment != null) { + FragmentCompat.setMenuVisibility(fragment, true); + FragmentCompat.setUserVisibleHint(fragment, true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + FragmentCompat.setMenuVisibility(f, false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java new file mode 100644 index 00000000000..01378d98153 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java @@ -0,0 +1,347 @@ +/* + * 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. + */ + +package android.support.v13.app; + +import java.util.ArrayList; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; + +/** + * Version of {@link android.support.v4.app.FragmentTabHost} that can be + * used with the platform {@link android.app.Fragment} APIs. You will not + * normally use this, instead using action bar tabs. + */ +public class FragmentTabHost extends TabHost + implements TabHost.OnTabChangeListener { + private final ArrayList mTabs = new ArrayList(); + private FrameLayout mRealTabContent; + private Context mContext; + private FragmentManager mFragmentManager; + private int mContainerId; + private TabHost.OnTabChangeListener mOnTabChangeListener; + private TabInfo mLastTab; + private boolean mAttached; + + static final class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + static class SavedState extends BaseSavedState { + String curTab; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + curTab = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(curTab); + } + + @Override + public String toString() { + return "FragmentTabHost.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " curTab=" + curTab + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public FragmentTabHost(Context context) { + // Note that we call through to the version that takes an AttributeSet, + // because the simple Context construct can result in a broken object! + super(context, null); + initFragmentTabHost(context, null); + } + + public FragmentTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + initFragmentTabHost(context, attrs); + } + + private void initFragmentTabHost(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.inflatedId }, 0, 0); + mContainerId = a.getResourceId(0, 0); + a.recycle(); + + super.setOnTabChangedListener(this); + } + + private void ensureHierarchy(Context context) { + // If owner hasn't made its own view hierarchy, then as a convenience + // we will construct a standard one here. + if (findViewById(android.R.id.tabs) == null) { + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + addView(ll, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + + TabWidget tw = new TabWidget(context); + tw.setId(android.R.id.tabs); + tw.setOrientation(TabWidget.HORIZONTAL); + ll.addView(tw, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0)); + + FrameLayout fl = new FrameLayout(context); + fl.setId(android.R.id.tabcontent); + ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); + + mRealTabContent = fl = new FrameLayout(context); + mRealTabContent.setId(mContainerId); + ll.addView(fl, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.FILL_PARENT, 0, 1)); + } + } + + /** + * @deprecated Don't call the original TabHost setup, you must instead + * call {@link #setup(Context, FragmentManager)} or + * {@link #setup(Context, FragmentManager, int)}. + */ + @Override @Deprecated + public void setup() { + throw new IllegalStateException( + "Must call setup() that takes a Context and FragmentManager"); + } + + public void setup(Context context, FragmentManager manager) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + ensureContent(); + } + + public void setup(Context context, FragmentManager manager, int containerId) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + mContainerId = containerId; + ensureContent(); + mRealTabContent.setId(containerId); + + // We must have an ID to be able to save/restore our state. If + // the owner hasn't set one at this point, we will set it ourself. + if (getId() == View.NO_ID) { + setId(android.R.id.tabhost); + } + } + + private void ensureContent() { + if (mRealTabContent == null) { + mRealTabContent = (FrameLayout)findViewById(mContainerId); + if (mRealTabContent == null) { + throw new IllegalStateException( + "No tab content FrameLayout found for id " + mContainerId); + } + } + } + + @Override + public void setOnTabChangedListener(OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + + if (mAttached) { + // If we are already attached to the window, then check to make + // sure this tab's fragment is inactive if it exists. This shouldn't + // normally happen. + info.fragment = mFragmentManager.findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + FragmentTransaction ft = mFragmentManager.beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + } + + mTabs.add(info); + addTab(tabSpec); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + String currentTab = getCurrentTabTag(); + + // Go through all tabs and make sure their fragments match + // the correct state. + FragmentTransaction ft = null; + for (int i=0; i + +Support classes to access some of the android.app package features introduced after API level 13 in a backwards compatible fashion. + + diff --git a/mobile/google-android-support-v4/.classpath b/mobile/google-android-support-v4/.classpath new file mode 100644 index 00000000000..6056dba1f7f --- /dev/null +++ b/mobile/google-android-support-v4/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mobile/google-android-support-v4/.gitignore b/mobile/google-android-support-v4/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/mobile/google-android-support-v4/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/mobile/google-android-support-v4/.project b/mobile/google-android-support-v4/.project new file mode 100644 index 00000000000..2ba2a9ebbf7 --- /dev/null +++ b/mobile/google-android-support-v4/.project @@ -0,0 +1,33 @@ + + + google-android-support-v4 + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs b/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..7f47c165658 --- /dev/null +++ b/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=ignore +org.eclipse.jdt.core.compiler.problem.deprecation=ignore +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=ignore +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=ignore +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=ignore +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml b/mobile/google-android-support-v4/AndroidManifest.xml similarity index 72% rename from mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml rename to mobile/google-android-support-v4/AndroidManifest.xml index 25053bee512..f27326972e4 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml +++ b/mobile/google-android-support-v4/AndroidManifest.xml @@ -1,5 +1,6 @@ - + - + - false + - + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml b/mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl similarity index 61% rename from mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml rename to mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl index fba21e1c06b..6d36b97eca3 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl @@ -1,7 +1,4 @@ - - - - - - 580dp - \ No newline at end of file +package android.support.v4.media; + +parcelable MediaMetadataCompat; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl new file mode 100644 index 00000000000..223fd5c96f7 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media; + +parcelable RatingCompat; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl new file mode 100644 index 00000000000..d0c2f6fce2c --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl @@ -0,0 +1,20 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable MediaSessionCompat.Token; +parcelable MediaSessionCompat.QueueItem; +parcelable MediaSessionCompat.ResultReceiverWrapper; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl new file mode 100644 index 00000000000..2e77c4fbde0 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable ParcelableVolumeInfo; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl new file mode 100644 index 00000000000..3d4ef5956a5 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable PlaybackStateCompat; diff --git a/mobile/google-android-support-v4/project.properties b/mobile/google-android-support-v4/project.properties new file mode 100644 index 00000000000..362a0a3082a --- /dev/null +++ b/mobile/google-android-support-v4/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-22 +android.library=true diff --git a/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF b/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java new file mode 100644 index 00000000000..abd208bb6f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.accessibilityservice; + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.view.View; + +/** + * Helper for accessing features in {@link android.accessibilityservice.AccessibilityService} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityServiceInfoCompat { + + static interface AccessibilityServiceInfoVersionImpl { + public String getId(AccessibilityServiceInfo info); + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info); + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info); + public String getDescription(AccessibilityServiceInfo info); + public String getSettingsActivityName(AccessibilityServiceInfo info); + public int getCapabilities(AccessibilityServiceInfo info); + } + + static class AccessibilityServiceInfoStubImpl implements AccessibilityServiceInfoVersionImpl { + + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return false; + } + + public String getDescription(AccessibilityServiceInfo info) { + return null; + } + + public String getId(AccessibilityServiceInfo info) { + return null; + } + + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return null; + } + + public String getSettingsActivityName(AccessibilityServiceInfo info) { + return null; + } + + public int getCapabilities(AccessibilityServiceInfo info) { + return 0; + } + } + + static class AccessibilityServiceInfoIcsImpl extends AccessibilityServiceInfoStubImpl { + + @Override + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getCanRetrieveWindowContent(info); + } + + @Override + public String getDescription(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getDescription(info); + } + + @Override + public String getId(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getId(info); + } + + @Override + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getResolveInfo(info); + } + + @Override + public String getSettingsActivityName(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getSettingsActivityName(info); + } + + @Override + public int getCapabilities(AccessibilityServiceInfo info) { + if (getCanRetrieveWindowContent(info)) { + return CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT; + } + return 0; + } + } + + static class AccessibilityServiceInfoJellyBeanMr2 extends AccessibilityServiceInfoIcsImpl { + @Override + public int getCapabilities(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatJellyBeanMr2.getCapabilities(info); + } + } + + static { + if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + IMPL = new AccessibilityServiceInfoJellyBeanMr2(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityServiceInfoIcsImpl(); + } else { + IMPL = new AccessibilityServiceInfoStubImpl(); + } + } + + // Capabilities + + private static final AccessibilityServiceInfoVersionImpl IMPL; + + /** + * Capability: This accessibility service can retrieve the active window content. + */ + public static final int CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 0x00000001; + + /** + * Capability: This accessibility service can request touch exploration mode in which + * touched items are spoken aloud and the UI can be explored via gestures. + */ + public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 0x00000002; + + /** + * Capability: This accessibility service can request enhanced web accessibility + * enhancements. For example, installing scripts to make app content more accessible. + */ + public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000004; + + /** + * Capability: This accessibility service can filter the key event stream. + */ + public static final int CAPABILITY_CAN_FILTER_KEY_EVENTS = 0x00000008; + + // Feedback types + + /** + * Denotes braille feedback. + */ + public static final int FEEDBACK_BRAILLE = 0x0000020; + + /** + * Mask for all feedback types. + * + * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN + * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC + * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE + * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_GENERIC + * @see FEEDBACK_BRAILLE + */ + public static final int FEEDBACK_ALL_MASK = 0xFFFFFFFF; + + // Flags + + /** + * If an {@link AccessibilityService} is the default for a given type. + * Default service is invoked only if no package specific one exists. In case of + * more than one package specific service only the earlier registered is notified. + */ + public static final int DEFAULT = 0x0000001; + + /** + * If this flag is set the system will regard views that are not important + * for accessibility in addition to the ones that are important for accessibility. + * That is, views that are marked as not important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO} or + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} and views that are + * marked as potentially important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_AUTO} for which the system has determined + * that are not important for accessibility, are both reported while querying the + * window content and also the accessibility service will receive accessibility events + * from them. + *

+ * Note: For accessibility services targeting API version + * {@link Build.VERSION_CODES#JELLY_BEAN} or higher this flag has to be explicitly + * set for the system to regard views that are not important for accessibility. For + * accessibility services targeting API version lower than + * {@link Build.VERSION_CODES#JELLY_BEAN} this flag is ignored and all views are + * regarded for accessibility purposes. + *

+ *

+ * Usually views not important for accessibility are layout managers that do not + * react to user actions, do not draw any content, and do not have any special + * semantics in the context of the screen content. For example, a three by three + * grid can be implemented as three horizontal linear layouts and one vertical, + * or three vertical linear layouts and one horizontal, or one grid layout, etc. + * In this context the actual layout mangers used to achieve the grid configuration + * are not important, rather it is important that there are nine evenly distributed + * elements. + *

+ */ + public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002; + + /** + * This flag requests that the system gets into touch exploration mode. + * In this mode a single finger moving on the screen behaves as a mouse + * pointer hovering over the user interface. The system will also detect + * certain gestures performed on the touch screen and notify this service. + * The system will enable touch exploration mode if there is at least one + * accessibility service that has this flag set. Hence, clearing this + * flag does not guarantee that the device will not be in touch exploration + * mode since there may be another enabled service that requested it. + *

+ * For accessibility services targeting API version higher than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} that want to set + * this flag have to declare this capability in their meta-data by setting + * the attribute canRequestTouchExplorationMode to true, otherwise this flag + * will be ignored. For how to declare the meta-data of a service refer to + * {@value AccessibilityService#SERVICE_META_DATA}. + *

+ *

+ * Services targeting API version equal to or lower than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} will work normally, i.e. + * the first time they are run, if this flag is specified, a dialog is + * shown to the user to confirm enabling explore by touch. + *

+ */ + public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x0000004; + + /** + * This flag requests from the system to enable web accessibility enhancing + * extensions. Such extensions aim to provide improved accessibility support + * for content presented in a {@link android.webkit.WebView}. An example of such + * an extension is injecting JavaScript from a secure source. The system will enable + * enhanced web accessibility if there is at least one accessibility service + * that has this flag set. Hence, clearing this flag does not guarantee that the + * device will not have enhanced web accessibility enabled since there may be + * another enabled service that requested it. + *

+ * Services that want to set this flag have to declare this capability + * in their meta-data by setting the attribute canRequestEnhancedWebAccessibility + * to true, otherwise this flag will be ignored. For how to declare the meta-data + * of a service refer to {@value AccessibilityService#SERVICE_META_DATA}. + *

+ */ + public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000008; + + /** + * This flag requests that the AccessibilityNodeInfos obtained + * by an {@link AccessibilityService} contain the id of the source view. + * The source view id will be a fully qualified resource name of the + * form "package:id/name", for example "foo.bar:id/my_list", and it is + * useful for UI test automation. This flag is not set by default. + */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000010; + + /** + * This flag requests from the system to filter key events. If this flag + * is set the accessibility service will receive the key events before + * applications allowing it implement global shortcuts. Setting this flag + * does not guarantee that this service will filter key events since only + * one service can do so at any given time. This avoids user confusion due + * to behavior change in case different key filtering services are enabled. + * If there is already another key filtering service enabled, this one will + * not receive key events. + *

+ * Services that want to set this flag have to declare this capability + * in their meta-data by setting the attribute canRequestFilterKeyEvents + * to true, otherwise this flag will be ignored. For how to declare the meta + * -data of a service refer to {@value AccessibilityService#SERVICE_META_DATA}. + *

+ */ + public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020; + + /* + * Hide constructor + */ + private AccessibilityServiceInfoCompat() { + + } + + /** + * The accessibility service id. + *

+ * Generated by the system. + *

+ * + * @return The id. + */ + public static String getId(AccessibilityServiceInfo info) { + return IMPL.getId(info); + } + + /** + * The service {@link ResolveInfo}. + *

+ * Generated by the system. + *

+ * + * @return The info. + */ + public static ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return IMPL.getResolveInfo(info); + } + + /** + * The settings activity name. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return The settings activity name. + */ + public static String getSettingsActivityName(AccessibilityServiceInfo info) { + return IMPL.getSettingsActivityName(info); + } + + /** + * Whether this service can retrieve the current window's content. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return True window content can be retrieved. + */ + public static boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return IMPL.getCanRetrieveWindowContent(info); + } + + /** + * Description of the accessibility service. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return The description. + */ + public static String getDescription(AccessibilityServiceInfo info) { + return IMPL.getDescription(info); + } + + /** + * Returns the string representation of a feedback type. For example, + * {@link AccessibilityServiceInfo#FEEDBACK_SPOKEN} is represented by the + * string FEEDBACK_SPOKEN. + * + * @param feedbackType The feedback type. + * @return The string representation. + */ + public static String feedbackTypeToString(int feedbackType) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + while (feedbackType > 0) { + final int feedbackTypeFlag = 1 << Integer.numberOfTrailingZeros(feedbackType); + feedbackType &= ~feedbackTypeFlag; + if (builder.length() > 1) { + builder.append(", "); + } + switch (feedbackTypeFlag) { + case AccessibilityServiceInfo.FEEDBACK_AUDIBLE: + builder.append("FEEDBACK_AUDIBLE"); + break; + case AccessibilityServiceInfo.FEEDBACK_HAPTIC: + builder.append("FEEDBACK_HAPTIC"); + break; + case AccessibilityServiceInfo.FEEDBACK_GENERIC: + builder.append("FEEDBACK_GENERIC"); + break; + case AccessibilityServiceInfo.FEEDBACK_SPOKEN: + builder.append("FEEDBACK_SPOKEN"); + break; + case AccessibilityServiceInfo.FEEDBACK_VISUAL: + builder.append("FEEDBACK_VISUAL"); + break; + } + } + builder.append("]"); + return builder.toString(); + } + + /** + * Returns the string representation of a flag. For example, + * {@link AccessibilityServiceInfo#DEFAULT} is represented by the + * string DEFAULT. + * + * @param flag The flag. + * @return The string representation. + */ + public static String flagToString(int flag) { + switch (flag) { + case DEFAULT: + return "DEFAULT"; + case FLAG_INCLUDE_NOT_IMPORTANT_VIEWS: + return "FLAG_INCLUDE_NOT_IMPORTANT_VIEWS"; + case FLAG_REQUEST_TOUCH_EXPLORATION_MODE: + return "FLAG_REQUEST_TOUCH_EXPLORATION_MODE"; + case FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY: + return "FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY"; + case FLAG_REPORT_VIEW_IDS: + return "FLAG_REPORT_VIEW_IDS"; + case FLAG_REQUEST_FILTER_KEY_EVENTS: + return "FLAG_REQUEST_FILTER_KEY_EVENTS"; + default: + return null; + } + } + + /** + * Returns the bit mask of capabilities this accessibility service has such as + * being able to retrieve the active window content, etc. + * + * @param info The service info whose capabilities to get. + * @return The capability bit mask. + * + * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT + * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION + * @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY + * @see #CAPABILITY_CAN_FILTER_KEY_EVENTS + */ + public static int getCapabilities(AccessibilityServiceInfo info) { + return IMPL.getCapabilities(info); + } + + /** + * Returns the string representation of a capability. For example, + * {@link #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT} is represented + * by the string CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT. + * + * @param capability The capability. + * @return The string representation. + */ + public static String capabilityToString(int capability) { + switch (capability) { + case CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT: + return "CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT"; + case CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION: + return "CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION"; + case CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY: + return "CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY"; + case CAPABILITY_CAN_FILTER_KEY_EVENTS: + return "CAPABILITY_CAN_FILTER_KEY_EVENTS"; + default: + return "UNKNOWN"; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java new file mode 100644 index 00000000000..ffbea0ade1c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.accessibilityservice; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; + +/** + * ICS implementation of the new APIs in AccessibilityServiceInfo. + */ +class AccessibilityServiceInfoCompatIcs { + + public static boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return info.getCanRetrieveWindowContent(); + } + + public static String getDescription(AccessibilityServiceInfo info) { + return info.getDescription(); + } + + public static String getId(AccessibilityServiceInfo info) { + return info.getId(); + } + + public static ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return info.getResolveInfo(); + } + + public static String getSettingsActivityName(AccessibilityServiceInfo info) { + return info.getSettingsActivityName(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java new file mode 100644 index 00000000000..3a41b7a2c90 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package android.support.v4.accessibilityservice; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; + +/** + * ICS implementation of the new APIs in AccessibilityServiceInfo. + */ +class AccessibilityServiceInfoCompatJellyBeanMr2 { + + public static int getCapabilities(AccessibilityServiceInfo info) { + return info.getCapabilities(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html new file mode 100644 index 00000000000..3d017b0ce0b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html @@ -0,0 +1,6 @@ + + +Support android.accessibilityservice classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java new file mode 100644 index 00000000000..5e3b954cde6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java @@ -0,0 +1,40 @@ +/* + * 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.v4.animation; + +import android.os.Build; + +abstract public class AnimatorCompatHelper { + + static AnimatorProvider IMPL; + + static { + if (Build.VERSION.SDK_INT >= 12) { + IMPL = new HoneycombMr1AnimatorCompatProvider(); + } else { + IMPL = new DonutAnimatorCompatProvider(); + } + } + + public static ValueAnimatorCompat emptyValueAnimator() { + return IMPL.emptyValueAnimator(); + } + + AnimatorCompatHelper() { + + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java new file mode 100644 index 00000000000..c70d6bc890b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java @@ -0,0 +1,57 @@ +/* + * 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.v4.animation; + +/** + *

An animation listener that receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * repetition of the animation.

+ * + * @hide + */ +public interface AnimatorListenerCompat { + + /** + *

Notifies the start of the animation.

+ * + * @param animation The started animation. + */ + void onAnimationStart(ValueAnimatorCompat animation); + + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which reached its end. + */ + void onAnimationEnd(ValueAnimatorCompat animation); + + /** + *

Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which was canceled. + */ + void onAnimationCancel(ValueAnimatorCompat animation); + + /** + *

Notifies the repetition of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationRepeat(ValueAnimatorCompat animation); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java new file mode 100644 index 00000000000..8f7567772d2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java @@ -0,0 +1,35 @@ +/* + * 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.v4.animation; + +/** + * A simple interface to do things in animation pulse. + *

+ * Before Honeycomb, it uses a simple Handler to mimic animation callback. + *

+ * This is only a minimal implementation which is why this class is hidden. + * + * @hide + */ +interface AnimatorProvider { + + /** + * Provides a simple ValueAnimator w/o any start or end values. It provides the same + * Animator callback interface. + */ + ValueAnimatorCompat emptyValueAnimator(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java new file mode 100644 index 00000000000..acbcccf030a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java @@ -0,0 +1,36 @@ +/* + * 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.v4.animation; + +/** + * Implementors of this interface can add themselves as update listeners + * to a ValueAnimator instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ValueAnimator. + * + * @hide + */ +public interface AnimatorUpdateListenerCompat { + + /** + *

Notifies the occurrence of another frame of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationUpdate(ValueAnimatorCompat animation); + +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java new file mode 100644 index 00000000000..9ef590cc11f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java @@ -0,0 +1,151 @@ +/* + * 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.v4.animation; + +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides similar functionality to Animators on platforms prior to Honeycomb. + *

+ * This is not a fully implemented API which is why it is not public. + * + * @hide + */ +class DonutAnimatorCompatProvider implements AnimatorProvider { + + @Override + public ValueAnimatorCompat emptyValueAnimator() { + return new DonutFloatValueAnimator(); + } + + private static class DonutFloatValueAnimator implements ValueAnimatorCompat { + + List mListeners = new ArrayList(); + List mUpdateListeners + = new ArrayList(); + View mTarget; + private long mStartTime; + private long mDuration = 200; + private float mFraction = 0f; + + private boolean mStarted = false; + private boolean mEnded = false; + + public DonutFloatValueAnimator() { + } + + private Runnable mLoopRunnable = new Runnable() { + @Override + public void run() { + long dt = getTime() - mStartTime; + float fraction = dt * 1f / mDuration; + if (fraction > 1f || mTarget.getParent() == null) { + fraction = 1f; + } + mFraction = fraction; + notifyUpdateListeners(); + if (mFraction >= 1f) { + dispatchEnd(); + } else { + mTarget.postDelayed(mLoopRunnable, 16); + } + } + }; + + private void notifyUpdateListeners() { + for (int i = mUpdateListeners.size() - 1; i >= 0; i--) { + mUpdateListeners.get(i).onAnimationUpdate(this); + } + } + + @Override + public void setTarget(View view) { + mTarget = view; + } + + @Override + public void addListener(AnimatorListenerCompat listener) { + mListeners.add(listener); + } + + @Override + public void setDuration(long duration) { + if (!mStarted) { + mDuration = duration; + } + } + + @Override + public void start() { + if (mStarted) { + return; + } + mStarted = true; + dispatchStart(); + mFraction = 0f; + mStartTime = getTime(); + mTarget.postDelayed(mLoopRunnable, 16); + } + + private long getTime() { + return mTarget.getDrawingTime(); + } + + private void dispatchStart() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationStart(this); + } + } + + private void dispatchEnd() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationEnd(this); + } + } + + private void dispatchCancel() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationCancel(this); + } + } + + @Override + public void cancel() { + if (mEnded) { + return; + } + mEnded = true; + if (mStarted) { + dispatchCancel(); + } + dispatchEnd(); + } + + @Override + public void addUpdateListener(AnimatorUpdateListenerCompat animatorUpdateListener) { + mUpdateListeners.add(animatorUpdateListener); + } + + @Override + public float getAnimatedFraction() { + return mFraction; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java new file mode 100644 index 00000000000..cc644814342 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java @@ -0,0 +1,124 @@ +/* + * 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.v4.animation; + +import android.animation.Animator; +import android.animation.PropertyValuesHolder; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.view.View; + +/** + * Uses framework Animators to provide ValueAnimatorCompat interface. + *

+ * This is not a fully implemented API which is why it is not public. + * + * @hide + */ +class HoneycombMr1AnimatorCompatProvider implements AnimatorProvider { + + @Override + public ValueAnimatorCompat emptyValueAnimator() { + return new HoneycombValueAnimatorCompat(ValueAnimator.ofFloat(0f, 1f)); + } + + static class HoneycombValueAnimatorCompat implements ValueAnimatorCompat { + + final Animator mWrapped; + + public HoneycombValueAnimatorCompat(Animator wrapped) { + mWrapped = wrapped; + } + + @Override + public void setTarget(View view) { + mWrapped.setTarget(view); + } + + @Override + public void addListener(AnimatorListenerCompat listener) { + mWrapped.addListener(new AnimatorListenerCompatWrapper(listener, this)); + } + + @Override + public void setDuration(long duration) { + mWrapped.setDuration(duration); + } + + @Override + public void start() { + mWrapped.start(); + } + + @Override + public void cancel() { + mWrapped.cancel(); + } + + @Override + public void addUpdateListener(final AnimatorUpdateListenerCompat animatorUpdateListener) { + if (mWrapped instanceof ValueAnimator) { + ((ValueAnimator) mWrapped).addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + animatorUpdateListener + .onAnimationUpdate(HoneycombValueAnimatorCompat.this); + } + }); + } + } + + @Override + public float getAnimatedFraction() { + return ((ValueAnimator) mWrapped).getAnimatedFraction(); + } + } + + static class AnimatorListenerCompatWrapper implements Animator.AnimatorListener { + + final AnimatorListenerCompat mWrapped; + + final ValueAnimatorCompat mValueAnimatorCompat; + + public AnimatorListenerCompatWrapper( + AnimatorListenerCompat wrapped, ValueAnimatorCompat valueAnimatorCompat) { + mWrapped = wrapped; + mValueAnimatorCompat = valueAnimatorCompat; + } + + @Override + public void onAnimationStart(Animator animation) { + mWrapped.onAnimationStart(mValueAnimatorCompat); + } + + @Override + public void onAnimationEnd(Animator animation) { + mWrapped.onAnimationEnd(mValueAnimatorCompat); + } + + @Override + public void onAnimationCancel(Animator animation) { + mWrapped.onAnimationCancel(mValueAnimatorCompat); + } + + @Override + public void onAnimationRepeat(Animator animation) { + mWrapped.onAnimationRepeat(mValueAnimatorCompat); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java new file mode 100644 index 00000000000..6d98c977198 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java @@ -0,0 +1,41 @@ +/* + * 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.v4.animation; + +import android.view.View; + +/** + * Compatibility implementation for {@link android.animation.Animator}. + * + * @hide + */ +public interface ValueAnimatorCompat { + + public void setTarget(View view); + + public void addListener(AnimatorListenerCompat listener); + + public void setDuration(long duration); + + public void start(); + + public void cancel(); + + void addUpdateListener(AnimatorUpdateListenerCompat animatorUpdateListener); + + public float getAnimatedFraction(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java new file mode 100644 index 00000000000..a7e782ddc10 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java @@ -0,0 +1,560 @@ +/* + * 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. + */ + + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.DrawerLayout; +import android.view.MenuItem; +import android.view.View; + +/** + * @deprecated Please use ActionBarDrawerToggle in support-v7-appcompat. + * + *

+ * This class provides a handy way to tie together the functionality of + * {@link DrawerLayout} and the framework ActionBar to implement the recommended + * design for navigation drawers. + * + *

To use ActionBarDrawerToggle, create one in your Activity and call through + * to the following methods corresponding to your Activity callbacks:

+ * + *
    + *
  • {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}
  • + *
  • {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected}
  • + *
+ * + *

Call {@link #syncState()} from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator + * with the state of the linked DrawerLayout after onRestoreInstanceState + * has occurred.

+ * + *

ActionBarDrawerToggle can be used directly as a + * {@link DrawerLayout.DrawerListener}, or if you are already providing your own listener, + * call through to each of the listener methods from your own.

+ * + */ +@Deprecated +public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { + + /** + * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use + * with ActionBarDrawerToggle. + */ + public interface DelegateProvider { + + /** + * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity + * does not wish to override the default behavior. + */ + @Nullable + Delegate getDrawerToggleDelegate(); + } + + public interface Delegate { + /** + * @return Up indicator drawable as defined in the Activity's theme, or null if one is not + * defined. + */ + @Nullable + Drawable getThemeUpIndicator(); + + /** + * Set the Action Bar's up indicator drawable and content description. + * + * @param upDrawable - Drawable to set as up indicator + * @param contentDescRes - Content description to set + */ + void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes); + + /** + * Set the Action Bar's up indicator content description. + * + * @param contentDescRes - Content description to set + */ + void setActionBarDescription(@StringRes int contentDescRes); + } + + private interface ActionBarDrawerToggleImpl { + Drawable getThemeUpIndicator(Activity activity); + Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes); + Object setActionBarDescription(Object info, Activity activity, int contentDescRes); + } + + private static class ActionBarDrawerToggleImplBase implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return null; + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + // No action bar to set. + return info; + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + // No action bar to set + return info; + } + } + + private static class ActionBarDrawerToggleImplHC implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(activity); + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + return ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator(info, activity, + themeImage, contentDescRes); + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + return ActionBarDrawerToggleHoneycomb.setActionBarDescription(info, activity, + contentDescRes); + } + } + + private static class ActionBarDrawerToggleImplJellybeanMR2 + implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity); + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity, + themeImage, contentDescRes); + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity, + contentDescRes); + } + } + + private static final ActionBarDrawerToggleImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 18) { + IMPL = new ActionBarDrawerToggleImplJellybeanMR2(); + } else if (version >= 11) { + IMPL = new ActionBarDrawerToggleImplHC(); + } else { + IMPL = new ActionBarDrawerToggleImplBase(); + } + } + + /** Fraction of its total width by which to offset the toggle drawable. */ + private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; + + // android.R.id.home as defined by public API in v11 + private static final int ID_HOME = 0x0102002c; + + private final Activity mActivity; + private final Delegate mActivityImpl; + private final DrawerLayout mDrawerLayout; + private boolean mDrawerIndicatorEnabled = true; + private boolean mHasCustomUpIndicator; + + private Drawable mHomeAsUpIndicator; + private Drawable mDrawerImage; + private SlideDrawable mSlider; + private final int mDrawerImageResource; + private final int mOpenDrawerContentDescRes; + private final int mCloseDrawerContentDescRes; + + private Object mSetIndicatorInfo; + + /** + * Construct a new ActionBarDrawerToggle. + * + *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. + * The provided drawer indicator drawable will animate slightly off-screen as the drawer + * is opened, indicating that in the open state the drawer will move off-screen when pressed + * and in the closed state the drawer will move on-screen when pressed.

+ * + *

String resources must be provided to describe the open/close drawer actions for + * accessibility services.

+ * + * @param activity The Activity hosting the drawer + * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar + * @param drawerImageRes A Drawable resource to use as the drawer indicator + * @param openDrawerContentDescRes A String resource to describe the "open drawer" action + * for accessibility + * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action + * for accessibility + */ + public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, + @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, + @StringRes int closeDrawerContentDescRes) { + this(activity, drawerLayout, !assumeMaterial(activity), drawerImageRes, + openDrawerContentDescRes, closeDrawerContentDescRes); + } + + private static boolean assumeMaterial(Context context) { + return context.getApplicationInfo().targetSdkVersion >= 21 && + (Build.VERSION.SDK_INT >= 21); + } + + /** + * Construct a new ActionBarDrawerToggle. + * + *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. + * The provided drawer indicator drawable will animate slightly off-screen as the drawer + * is opened, indicating that in the open state the drawer will move off-screen when pressed + * and in the closed state the drawer will move on-screen when pressed.

+ * + *

String resources must be provided to describe the open/close drawer actions for + * accessibility services.

+ * + * @param activity The Activity hosting the drawer + * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar + * @param animate True to animate the drawer indicator along with the drawer's position. + * Material apps should set this to false. + * @param drawerImageRes A Drawable resource to use as the drawer indicator + * @param openDrawerContentDescRes A String resource to describe the "open drawer" action + * for accessibility + * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action + * for accessibility + */ + public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, boolean animate, + @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, + @StringRes int closeDrawerContentDescRes) { + mActivity = activity; + + // Allow the Activity to provide an impl + if (activity instanceof DelegateProvider) { + mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); + } else { + mActivityImpl = null; + } + + mDrawerLayout = drawerLayout; + mDrawerImageResource = drawerImageRes; + mOpenDrawerContentDescRes = openDrawerContentDescRes; + mCloseDrawerContentDescRes = closeDrawerContentDescRes; + + mHomeAsUpIndicator = getThemeUpIndicator(); + mDrawerImage = ContextCompat.getDrawable(activity, drawerImageRes); + mSlider = new SlideDrawable(mDrawerImage); + mSlider.setOffset(animate ? TOGGLE_DRAWABLE_OFFSET : 0); + } + + /** + * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. + * + *

This should be called from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after + * the DrawerLayout's instance state has been restored, and any other time when the state + * may have diverged in such a way that the ActionBarDrawerToggle was not notified. + * (For example, if you stop forwarding appropriate drawer events for a period of time.)

+ */ + public void syncState() { + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { + mSlider.setPosition(1); + } else { + mSlider.setPosition(0); + } + + if (mDrawerIndicatorEnabled) { + setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? + mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); + } + } + + /** + * Set the up indicator to display when the drawer indicator is not + * enabled. + *

+ * If you pass null to this method, the default drawable from + * the theme will be used. + * + * @param indicator A drawable to use for the up indicator, or null to use + * the theme's default + * @see #setDrawerIndicatorEnabled(boolean) + */ + public void setHomeAsUpIndicator(Drawable indicator) { + if (indicator == null) { + mHomeAsUpIndicator = getThemeUpIndicator(); + mHasCustomUpIndicator = false; + } else { + mHomeAsUpIndicator = indicator; + mHasCustomUpIndicator = true; + } + + if (!mDrawerIndicatorEnabled) { + setActionBarUpIndicator(mHomeAsUpIndicator, 0); + } + } + + /** + * Set the up indicator to display when the drawer indicator is not + * enabled. + *

+ * If you pass 0 to this method, the default drawable from the theme will + * be used. + * + * @param resId Resource ID of a drawable to use for the up indicator, or 0 + * to use the theme's default + * @see #setDrawerIndicatorEnabled(boolean) + */ + public void setHomeAsUpIndicator(int resId) { + Drawable indicator = null; + if (resId != 0) { + indicator = ContextCompat.getDrawable(mActivity, resId); + } + + setHomeAsUpIndicator(indicator); + } + + /** + * Enable or disable the drawer indicator. The indicator defaults to enabled. + * + *

When the indicator is disabled, the ActionBar will revert to displaying + * the home-as-up indicator provided by the Activity's theme in the + * android.R.attr.homeAsUpIndicator attribute instead of the animated + * drawer glyph.

+ * + * @param enable true to enable, false to disable + */ + public void setDrawerIndicatorEnabled(boolean enable) { + if (enable != mDrawerIndicatorEnabled) { + if (enable) { + setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? + mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); + } else { + setActionBarUpIndicator(mHomeAsUpIndicator, 0); + } + mDrawerIndicatorEnabled = enable; + } + } + + /** + * @return true if the enhanced drawer indicator is enabled, false otherwise + * @see #setDrawerIndicatorEnabled(boolean) + */ + public boolean isDrawerIndicatorEnabled() { + return mDrawerIndicatorEnabled; + } + + /** + * This method should always be called by your Activity's + * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} + * method. + * + * @param newConfig The new configuration + */ + public void onConfigurationChanged(Configuration newConfig) { + // Reload drawables that can change with configuration + if (!mHasCustomUpIndicator) { + mHomeAsUpIndicator = getThemeUpIndicator(); + } + mDrawerImage = ContextCompat.getDrawable(mActivity, mDrawerImageResource); + syncState(); + } + + /** + * This method should be called by your Activity's + * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. + * If it returns true, your onOptionsItemSelected method should return true and + * skip further processing. + * + * @param item the MenuItem instance representing the selected menu item + * @return true if the event was handled and further processing should not occur + */ + public boolean onOptionsItemSelected(MenuItem item) { + if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { + if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { + mDrawerLayout.closeDrawer(GravityCompat.START); + } else { + mDrawerLayout.openDrawer(GravityCompat.START); + } + return true; + } + return false; + } + + /** + * {@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. + * + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + float glyphOffset = mSlider.getPosition(); + if (slideOffset > 0.5f) { + glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); + } else { + glyphOffset = Math.min(glyphOffset, slideOffset * 2); + } + mSlider.setPosition(glyphOffset); + } + + /** + * {@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. + * + * @param drawerView Drawer view that is now open + */ + @Override + public void onDrawerOpened(View drawerView) { + mSlider.setPosition(1); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mCloseDrawerContentDescRes); + } + } + + /** + * {@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. + * + * @param drawerView Drawer view that is now closed + */ + @Override + public void onDrawerClosed(View drawerView) { + mSlider.setPosition(0); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mOpenDrawerContentDescRes); + } + } + + /** + * {@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. + * + * @param newState The new drawer motion state + */ + @Override + public void onDrawerStateChanged(int newState) { + } + + Drawable getThemeUpIndicator() { + if (mActivityImpl != null) { + return mActivityImpl.getThemeUpIndicator(); + } + return IMPL.getThemeUpIndicator(mActivity); + } + + void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); + return; + } + mSetIndicatorInfo = IMPL + .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes); + } + + void setActionBarDescription(int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarDescription(contentDescRes); + return; + } + mSetIndicatorInfo = IMPL + .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes); + } + + private class SlideDrawable extends InsetDrawable implements Drawable.Callback { + private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; + private final Rect mTmpRect = new Rect(); + + private float mPosition; + private float mOffset; + + private SlideDrawable(Drawable wrapped) { + super(wrapped, 0); + } + + /** + * Sets the current position along the offset. + * + * @param position a value between 0 and 1 + */ + public void setPosition(float position) { + mPosition = position; + invalidateSelf(); + } + + public float getPosition() { + return mPosition; + } + + /** + * Specifies the maximum offset when the position is at 1. + * + * @param offset maximum offset as a fraction of the drawable width, + * positive to shift left or negative to shift right. + * @see #setPosition(float) + */ + public void setOffset(float offset) { + mOffset = offset; + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + copyBounds(mTmpRect); + canvas.save(); + + // Layout direction must be obtained from the activity. + final boolean isLayoutRTL = ViewCompat.getLayoutDirection( + mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; + final int flipRtl = isLayoutRTL ? -1 : 1; + final int width = mTmpRect.width(); + canvas.translate(-mOffset * width * mPosition * flipRtl, 0); + + // Force auto-mirroring if it's not supported by the platform. + if (isLayoutRTL && !mHasMirroring) { + canvas.translate(width, 0); + canvas.scale(-1, 1); + } + + super.draw(canvas); + canvas.restore(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java new file mode 100644 index 00000000000..bd279546c0e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java @@ -0,0 +1,140 @@ +/* + * 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. + */ + + +package android.support.v4.app; + +import android.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.lang.reflect.Method; + +/** + * This class encapsulates some awful hacks. + * + * Before JB-MR2 (API 18) it was not possible to change the home-as-up indicator glyph + * in an action bar without some really gross hacks. Since the MR2 SDK is not published as of + * this writing, the new API is accessed via reflection here if available. + */ +class ActionBarDrawerToggleHoneycomb { + private static final String TAG = "ActionBarDrawerToggleHoneycomb"; + + private static final int[] THEME_ATTRS = new int[] { + R.attr.homeAsUpIndicator + }; + + public static Object setActionBarUpIndicator(Object info, Activity activity, + Drawable drawable, int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + final SetIndicatorInfo sii = (SetIndicatorInfo) info; + if (sii.setHomeAsUpIndicator != null) { + try { + final ActionBar actionBar = activity.getActionBar(); + sii.setHomeAsUpIndicator.invoke(actionBar, drawable); + sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); + } catch (Exception e) { + Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e); + } + } else if (sii.upIndicatorView != null) { + sii.upIndicatorView.setImageDrawable(drawable); + } else { + Log.w(TAG, "Couldn't set home-as-up indicator"); + } + return info; + } + + public static Object setActionBarDescription(Object info, Activity activity, + int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + final SetIndicatorInfo sii = (SetIndicatorInfo) info; + if (sii.setHomeAsUpIndicator != null) { + try { + final ActionBar actionBar = activity.getActionBar(); + sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); + if (Build.VERSION.SDK_INT <= 19) { + // For API 19 and earlier, we need to manually force the + // action bar to generate a new content description. + actionBar.setSubtitle(actionBar.getSubtitle()); + } + } catch (Exception e) { + Log.w(TAG, "Couldn't set content description via JB-MR2 API", e); + } + } + return info; + } + + public static Drawable getThemeUpIndicator(Activity activity) { + final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS); + final Drawable result = a.getDrawable(0); + a.recycle(); + return result; + } + + private static class SetIndicatorInfo { + public Method setHomeAsUpIndicator; + public Method setHomeActionContentDescription; + public ImageView upIndicatorView; + + SetIndicatorInfo(Activity activity) { + try { + setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", + Drawable.class); + setHomeActionContentDescription = ActionBar.class.getDeclaredMethod( + "setHomeActionContentDescription", Integer.TYPE); + + // If we got the method we won't need the stuff below. + return; + } catch (NoSuchMethodException e) { + // Oh well. We'll use the other mechanism below instead. + } + + final View home = activity.findViewById(android.R.id.home); + if (home == null) { + // Action bar doesn't have a known configuration, an OEM messed with things. + return; + } + + final ViewGroup parent = (ViewGroup) home.getParent(); + final int childCount = parent.getChildCount(); + if (childCount != 2) { + // No idea which one will be the right one, an OEM messed with things. + return; + } + + final View first = parent.getChildAt(0); + final View second = parent.getChildAt(1); + final View up = first.getId() == android.R.id.home ? second : first; + + if (up instanceof ImageView) { + // Jackpot! (Probably...) + upIndicatorView = (ImageView) up; + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java new file mode 100644 index 00000000000..5d98d8856cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + + +package android.support.v4.app; + +import android.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.Log; + +class ActionBarDrawerToggleJellybeanMR2 { + private static final String TAG = "ActionBarDrawerToggleImplJellybeanMR2"; + + private static final int[] THEME_ATTRS = new int[] { + R.attr.homeAsUpIndicator + }; + + public static Object setActionBarUpIndicator(Object info, Activity activity, + Drawable drawable, int contentDescRes) { + final ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + actionBar.setHomeAsUpIndicator(drawable); + actionBar.setHomeActionContentDescription(contentDescRes); + } + return info; + } + + public static Object setActionBarDescription(Object info, Activity activity, + int contentDescRes) { + final ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + actionBar.setHomeActionContentDescription(contentDescRes); + } + return info; + } + + public static Drawable getThemeUpIndicator(Activity activity) { + final ActionBar actionBar = activity.getActionBar(); + final Context context; + if (actionBar != null) { + context = actionBar.getThemedContext(); + } else { + context = activity; + } + + final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS, + R.attr.actionBarStyle, 0); + final Drawable result = a.getDrawable(0); + a.recycle(); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java new file mode 100644 index 00000000000..8d8bf04b599 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.view.View; + +import java.util.List; +import java.util.Map; + +/** + * Helper for accessing features in {@link android.app.Activity} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ActivityCompat extends ContextCompat { + /** + * Invalidate the activity's options menu, if able. + * + *

Before API level 11 (Android 3.0/Honeycomb) the lifecycle of the + * options menu was controlled primarily by the user's operation of + * the hardware menu key. When the user presses down on the menu key + * for the first time the menu was created and prepared by calls + * to {@link Activity#onCreateOptionsMenu(android.view.Menu)} and + * {@link Activity#onPrepareOptionsMenu(android.view.Menu)} respectively. + * Subsequent presses of the menu key kept the existing instance of the + * Menu itself and called {@link Activity#onPrepareOptionsMenu(android.view.Menu)} + * to give the activity an opportunity to contextually alter the menu + * before the menu panel was shown.

+ * + *

In Android 3.0+ the Action Bar forces the options menu to be built early + * so that items chosen to show as actions may be displayed when the activity + * first becomes visible. The Activity method invalidateOptionsMenu forces + * the entire menu to be destroyed and recreated from + * {@link Activity#onCreateOptionsMenu(android.view.Menu)}, offering a similar + * though heavier-weight opportunity to change the menu's contents. Normally + * this functionality is used to support a changing configuration of Fragments.

+ * + *

Applications may use this support helper to signal a significant change in + * activity state that should cause the options menu to be rebuilt. If the app + * is running on an older platform version that does not support menu invalidation + * the app will still receive {@link Activity#onPrepareOptionsMenu(android.view.Menu)} + * the next time the user presses the menu key and this method will return false. + * If this method returns true the options menu was successfully invalidated.

+ * + * @param activity Invalidate the options menu of this activity + * @return true if this operation was supported and it completed; false if it was not available. + */ + public static boolean invalidateOptionsMenu(Activity activity) { + if (Build.VERSION.SDK_INT >= 11) { + ActivityCompatHoneycomb.invalidateOptionsMenu(activity); + return true; + } + return false; + } + + /** + * Start an activity with additional launch information, if able. + * + *

In Android 4.1+ additional options were introduced to allow for more + * control on activity launch animations. Applications can use this method + * along with {@link ActivityOptionsCompat} to use these animations when + * available. When run on versions of the platform where this feature does + * not exist the activity will be launched normally.

+ * + * @param activity Context to launch activity from. + * @param intent The description of the activity to start. + * @param options Additional options for how the Activity should be started. + * May be null if there are no options. See + * {@link ActivityOptionsCompat} for how to build the Bundle + * supplied here; there are no supported definitions for + * building it manually. + */ + public static void startActivity(Activity activity, Intent intent, @Nullable Bundle options) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.startActivity(activity, intent, options); + } else { + activity.startActivity(intent); + } + } + + /** + * Start new activity with options, if able, for which you would like a + * result when it finished. + * + *

In Android 4.1+ additional options were introduced to allow for more + * control on activity launch animations. Applications can use this method + * along with {@link ActivityOptionsCompat} to use these animations when + * available. When run on versions of the platform where this feature does + * not exist the activity will be launched normally.

+ * + * @param activity Origin activity to launch from. + * @param intent The description of the activity to start. + * @param requestCode If >= 0, this code will be returned in + * onActivityResult() when the activity exits. + * @param options Additional options for how the Activity should be started. + * May be null if there are no options. See + * {@link ActivityOptionsCompat} for how to build the Bundle + * supplied here; there are no supported definitions for + * building it manually. + */ + public static void startActivityForResult(Activity activity, Intent intent, int requestCode, + @Nullable Bundle options) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.startActivityForResult(activity, intent, requestCode, options); + } else { + activity.startActivityForResult(intent, requestCode); + } + } + + /** + * Finish this activity, and tries to finish all activities immediately below it + * in the current task that have the same affinity. + * + *

On Android 4.1+ calling this method will call through to the native version of this + * method. For other platforms {@link Activity#finish()} will be called instead.

+ */ + public static void finishAffinity(Activity activity) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.finishAffinity(activity); + } else { + activity.finish(); + } + } + + /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link Activity#finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * + *

On Android 4.4 or lower, this method only finishes the Activity with no + * special exit transition.

+ */ + public static void finishAfterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.finishAfterTransition(activity); + } else { + activity.finish(); + } + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launched Activity. This requires + * {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launched Activity. + */ + public static void setEnterSharedElementCallback(Activity activity, + SharedElementCallback callback) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.setEnterSharedElementCallback(activity, createCallback(callback)); + } + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launching Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launching Activity. + */ + public static void setExitSharedElementCallback(Activity activity, + SharedElementCallback callback) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.setExitSharedElementCallback(activity, createCallback(callback)); + } + } + + public static void postponeEnterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.postponeEnterTransition(activity); + } + } + + public static void startPostponedEnterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.startPostponedEnterTransition(activity); + } + } + + private static ActivityCompat21.SharedElementCallback21 createCallback( + SharedElementCallback callback) { + ActivityCompat21.SharedElementCallback21 newCallback = null; + if (callback != null) { + newCallback = new ActivityCompat.SharedElementCallback21Impl(callback); + } + return newCallback; + } + + private static class SharedElementCallback21Impl + extends ActivityCompat21.SharedElementCallback21 { + + private SharedElementCallback mCallback; + + public SharedElementCallback21Impl(SharedElementCallback callback) { + mCallback = callback; + } + + @Override + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) { + mCallback.onSharedElementStart(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + mCallback.onSharedElementEnd(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onRejectSharedElements(List rejectedSharedElements) { + mCallback.onRejectSharedElements(rejectedSharedElements); + } + + @Override + public void onMapSharedElements(List names, Map sharedElements) { + mCallback.onMapSharedElements(names, sharedElements); + } + + @Override + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, RectF screenBounds) { + return mCallback.onCaptureSharedElementSnapshot(sharedElement, viewToGlobalMatrix, + screenBounds); + } + + @Override + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + return mCallback.onCreateSnapshotView(context, snapshot); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java new file mode 100644 index 00000000000..f690fff0ed2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java @@ -0,0 +1,124 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.app.SharedElementCallback; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Parcelable; +import android.view.View; + +import java.lang.Override; +import java.lang.String; +import java.util.List; +import java.util.Map; + +class ActivityCompat21 { + + public static void finishAfterTransition(Activity activity) { + activity.finishAfterTransition(); + } + + public static void setEnterSharedElementCallback(Activity activity, + SharedElementCallback21 callback) { + activity.setEnterSharedElementCallback(createCallback(callback)); + } + + public static void setExitSharedElementCallback(Activity activity, + SharedElementCallback21 callback) { + activity.setExitSharedElementCallback(createCallback(callback)); + } + + public static void postponeEnterTransition(Activity activity) { + activity.postponeEnterTransition(); + } + + public static void startPostponedEnterTransition(Activity activity) { + activity.startPostponedEnterTransition(); + } + + public abstract static class SharedElementCallback21 { + public abstract void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots); + + public abstract void onSharedElementEnd(List sharedElementNames, + List sharedElements, List sharedElementSnapshots); + + public abstract void onRejectSharedElements(List rejectedSharedElements); + + public abstract void onMapSharedElements(List names, + Map sharedElements); + public abstract Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, RectF screenBounds); + public abstract View onCreateSnapshotView(Context context, Parcelable snapshot); + } + + private static SharedElementCallback createCallback(SharedElementCallback21 callback) { + SharedElementCallback newListener = null; + if (callback != null) { + newListener = new SharedElementCallbackImpl(callback); + } + return newListener; + } + + private static class SharedElementCallbackImpl extends SharedElementCallback { + private SharedElementCallback21 mCallback; + + public SharedElementCallbackImpl(SharedElementCallback21 callback) { + mCallback = callback; + } + + @Override + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) { + mCallback.onSharedElementStart(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + mCallback.onSharedElementEnd(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onRejectSharedElements(List rejectedSharedElements) { + mCallback.onRejectSharedElements(rejectedSharedElements); + } + + @Override + public void onMapSharedElements(List names, Map sharedElements) { + mCallback.onMapSharedElements(names, sharedElements); + } + + @Override + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, + RectF screenBounds) { + return mCallback.onCaptureSharedElementSnapshot(sharedElement, viewToGlobalMatrix, + screenBounds); + } + + @Override + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + return mCallback.onCreateSnapshotView(context, snapshot); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java new file mode 100644 index 00000000000..615bc4415f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Implementation of activity compatibility that can call Honeycomb APIs. + */ +class ActivityCompatHoneycomb { + static void invalidateOptionsMenu(Activity activity) { + activity.invalidateOptionsMenu(); + } + + static void dump(Activity activity, String prefix, FileDescriptor fd, + PrintWriter writer, String[] args) { + activity.dump(prefix, fd, writer, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java new file mode 100644 index 00000000000..0c7318fa290 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class ActivityCompatJB { + public static void startActivity(Context context, Intent intent, Bundle options) { + context.startActivity(intent, options); + } + + public static void startActivityForResult(Activity activity, Intent intent, int requestCode, Bundle options) { + activity.startActivityForResult(intent, requestCode, options); + } + + public static void finishAffinity(Activity activity) { + activity.finishAffinity(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java new file mode 100644 index 00000000000..ff5c18033f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.ActivityManager; +import android.os.Build; +import android.support.annotation.NonNull; + +/** + * Helper for accessing features in {@link android.app.ActivityManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public final class ActivityManagerCompat { + private ActivityManagerCompat() { + } + + /** + * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM + * is ultimately up to the device configuration, but currently it generally means + * something in the class of a 512MB device with about a 800x480 or less screen. + * This is mostly intended to be used by apps to determine whether they should turn + * off certain features that require more RAM. + */ + public static boolean isLowRamDevice(@NonNull ActivityManager am) { + if (Build.VERSION.SDK_INT >= 19) { + return ActivityManagerCompatKitKat.isLowRamDevice(am); + } + return false; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java new file mode 100644 index 00000000000..3f889f637ba --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.ActivityManager; + +class ActivityManagerCompatKitKat { + public static boolean isLowRamDevice(ActivityManager am) { + return am.isLowRamDevice(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java new file mode 100644 index 00000000000..6eb93c7268b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java @@ -0,0 +1,242 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.view.View; + +/** + * Helper for accessing features in {@link android.app.ActivityOptions} + * introduced in API level 16 in a backwards compatible fashion. + */ +public class ActivityOptionsCompat { + /** + * Create an ActivityOptions specifying a custom animation to run when the + * activity is displayed. + * + * @param context Who is defining this. This is the application that the + * animation resources will be loaded from. + * @param enterResId A resource ID of the animation resource to use for the + * incoming activity. Use 0 for no animation. + * @param exitResId A resource ID of the animation resource to use for the + * outgoing activity. Use 0 for no animation. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeCustomAnimation(Context context, + int enterResId, int exitResId) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeCustomAnimation(context, enterResId, exitResId)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions specifying an animation where the new activity is + * scaled from a small originating area of the screen to its final full + * representation. + *

+ * If the Intent this is being used with has not set its + * {@link android.content.Intent#setSourceBounds(android.graphics.Rect)}, + * those bounds will be filled in for you based on the initial bounds passed + * in here. + * + * @param source The View that the new activity is animating from. This + * defines the coordinate space for startX and startY. + * @param startX The x starting location of the new activity, relative to + * source. + * @param startY The y starting location of the activity, relative to source. + * @param startWidth The initial width of the new activity. + * @param startHeight The initial height of the new activity. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeScaleUpAnimation(View source, + int startX, int startY, int startWidth, int startHeight) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeScaleUpAnimation(source, startX, startY, + startWidth, startHeight)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions specifying an animation where a thumbnail is + * scaled from a given position to the new activity window that is being + * started. + *

+ * If the Intent this is being used with has not set its + * {@link android.content.Intent#setSourceBounds(android.graphics.Rect)}, + * those bounds will be filled in for you based on the initial thumbnail + * location and size provided here. + * + * @param source The View that this thumbnail is animating from. This + * defines the coordinate space for startX and startY. + * @param thumbnail The bitmap that will be shown as the initial thumbnail + * of the animation. + * @param startX The x starting location of the bitmap, relative to source. + * @param startY The y starting location of the bitmap, relative to source. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(View source, + Bitmap thumbnail, int startX, int startY) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeThumbnailScaleUpAnimation(source, thumbnail, + startX, startY)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of one shared element to the started Activity. + * The position of sharedElement will be used as the epicenter for the + * exit Transition. The position of the shared element in the launched Activity will be the + * epicenter of its entering Transition. + * + *

This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.

+ * @param activity The Activity whose window contains the shared elements. + * @param sharedElement The View to transition to the started Activity. sharedElement must + * have a non-null sharedElementName. + * @param sharedElementName The shared element name as used in the target Activity. This may + * be null if it has the same name as sharedElement. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, + View sharedElement, String sharedElementName) { + if (Build.VERSION.SDK_INT >= 21) { + return new ActivityOptionsCompat.ActivityOptionsImpl21( + ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, + sharedElement, sharedElementName)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of multiple shared elements to the started + * Activity. The position of the first element in sharedElements + * will be used as the epicenter for the exit Transition. The position of the associated + * shared element in the launched Activity will be the epicenter of its entering Transition. + * + *

This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.

+ * @param activity The Activity whose window contains the shared elements. + * @param sharedElements The names of the shared elements to transfer to the called + * Activity and their associated Views. The Views must each have + * a unique shared element name. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, + Pair... sharedElements) { + if (Build.VERSION.SDK_INT >= 21) { + View[] views = null; + String[] names = null; + if (sharedElements != null) { + views = new View[sharedElements.length]; + names = new String[sharedElements.length]; + for (int i = 0; i < sharedElements.length; i++) { + views[i] = sharedElements[i].first; + names[i] = sharedElements[i].second; + } + } + return new ActivityOptionsCompat.ActivityOptionsImpl21( + ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, views, names)); + } + return new ActivityOptionsCompat(); + } + + private static class ActivityOptionsImplJB extends ActivityOptionsCompat { + private final ActivityOptionsCompatJB mImpl; + + ActivityOptionsImplJB(ActivityOptionsCompatJB impl) { + mImpl = impl; + } + + @Override + public Bundle toBundle() { + return mImpl.toBundle(); + } + + @Override + public void update(ActivityOptionsCompat otherOptions) { + if (otherOptions instanceof ActivityOptionsImplJB) { + ActivityOptionsImplJB otherImpl = (ActivityOptionsImplJB)otherOptions; + mImpl.update(otherImpl.mImpl); + } + } + } + + private static class ActivityOptionsImpl21 extends ActivityOptionsCompat { + private final ActivityOptionsCompat21 mImpl; + + ActivityOptionsImpl21(ActivityOptionsCompat21 impl) { + mImpl = impl; + } + + @Override + public Bundle toBundle() { + return mImpl.toBundle(); + } + + @Override + public void update(ActivityOptionsCompat otherOptions) { + if (otherOptions instanceof ActivityOptionsCompat.ActivityOptionsImpl21) { + ActivityOptionsCompat.ActivityOptionsImpl21 + otherImpl = (ActivityOptionsCompat.ActivityOptionsImpl21)otherOptions; + mImpl.update(otherImpl.mImpl); + } + } + } + + protected ActivityOptionsCompat() { + } + + /** + * Returns the created options as a Bundle, which can be passed to + * {@link ActivityCompat#startActivity(android.app.Activity, android.content.Intent, android.os.Bundle)}. + * Note that the returned Bundle is still owned by the ActivityOptions + * object; you must not modify it, but can supply it to the startActivity + * methods that take an options Bundle. + */ + public Bundle toBundle() { + return null; + } + + /** + * Update the current values in this ActivityOptions from those supplied in + * otherOptions. Any values defined in otherOptions replace those in the + * base options. + */ + public void update(ActivityOptionsCompat otherOptions) { + // Do nothing. + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java new file mode 100644 index 00000000000..d629e653e14 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.ActivityOptions; +import android.app.Activity; +import android.os.Bundle; +import android.util.Pair; +import android.view.View; + +class ActivityOptionsCompat21 { + + private final ActivityOptions mActivityOptions; + + public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity, + View sharedElement, String sharedElementName) { + return new ActivityOptionsCompat21( + ActivityOptions.makeSceneTransitionAnimation(activity, sharedElement, + sharedElementName)); + } + + public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity, + View[] sharedElements, String[] sharedElementNames) { + Pair[] pairs = null; + if (sharedElements != null) { + pairs = new Pair[sharedElements.length]; + for (int i = 0; i < pairs.length; i++) { + pairs[i] = Pair.create(sharedElements[i], sharedElementNames[i]); + } + } + return new ActivityOptionsCompat21( + ActivityOptions.makeSceneTransitionAnimation(activity, pairs)); + } + + private ActivityOptionsCompat21(ActivityOptions activityOptions) { + mActivityOptions = activityOptions; + } + + public Bundle toBundle() { + return mActivityOptions.toBundle(); + } + + public void update(ActivityOptionsCompat21 otherOptions) { + mActivityOptions.update(otherOptions.mActivityOptions); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java new file mode 100644 index 00000000000..df9d9879582 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.ActivityOptions; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.View; + +class ActivityOptionsCompatJB { + + public static ActivityOptionsCompatJB makeCustomAnimation(Context context, + int enterResId, int exitResId) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeCustomAnimation(context, enterResId, exitResId)); + } + + public static ActivityOptionsCompatJB makeScaleUpAnimation(View source, + int startX, int startY, int startWidth, int startHeight) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeScaleUpAnimation(source, startX, startY, startWidth, startHeight)); + } + + public static ActivityOptionsCompatJB makeThumbnailScaleUpAnimation(View source, + Bitmap thumbnail, int startX, int startY) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeThumbnailScaleUpAnimation(source, thumbnail, startX, startY)); + } + + private final ActivityOptions mActivityOptions; + + private ActivityOptionsCompatJB(ActivityOptions activityOptions) { + mActivityOptions = activityOptions; + } + + public Bundle toBundle() { + return mActivityOptions.toBundle(); + } + + public void update(ActivityOptionsCompatJB otherOptions) { + mActivityOptions.update(otherOptions.mActivityOptions); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java b/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java new file mode 100644 index 00000000000..25f3ebfba06 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java @@ -0,0 +1,1457 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.graphics.Rect; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.LogWriter; +import android.support.v4.util.Pair; +import android.support.v4.util.ArrayMap; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; + +final class BackStackState implements Parcelable { + final int[] mOps; + final int mTransition; + final int mTransitionStyle; + final String mName; + final int mIndex; + final int mBreadCrumbTitleRes; + final CharSequence mBreadCrumbTitleText; + final int mBreadCrumbShortTitleRes; + final CharSequence mBreadCrumbShortTitleText; + final ArrayList mSharedElementSourceNames; + final ArrayList mSharedElementTargetNames; + + public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) { + int numRemoved = 0; + BackStackRecord.Op op = bse.mHead; + while (op != null) { + if (op.removed != null) numRemoved += op.removed.size(); + op = op.next; + } + mOps = new int[bse.mNumOp*7 + numRemoved]; + + if (!bse.mAddToBackStack) { + throw new IllegalStateException("Not on back stack"); + } + + op = bse.mHead; + int pos = 0; + while (op != null) { + mOps[pos++] = op.cmd; + mOps[pos++] = op.fragment != null ? op.fragment.mIndex : -1; + mOps[pos++] = op.enterAnim; + mOps[pos++] = op.exitAnim; + mOps[pos++] = op.popEnterAnim; + mOps[pos++] = op.popExitAnim; + if (op.removed != null) { + final int N = op.removed.size(); + mOps[pos++] = N; + for (int i=0; i= 0) { + Fragment f = fm.mActive.get(findex); + op.fragment = f; + } else { + op.fragment = null; + } + op.enterAnim = mOps[pos++]; + op.exitAnim = mOps[pos++]; + op.popEnterAnim = mOps[pos++]; + op.popExitAnim = mOps[pos++]; + final int N = mOps[pos++]; + if (N > 0) { + op.removed = new ArrayList(N); + for (int i=0; i CREATOR + = new Parcelable.Creator() { + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} + +/** + * @hide Entry of an operation on the fragment back stack. + */ +final class BackStackRecord extends FragmentTransaction implements + FragmentManager.BackStackEntry, Runnable { + static final String TAG = FragmentManagerImpl.TAG; + static final boolean SUPPORTS_TRANSITIONS = Build.VERSION.SDK_INT >= 21; + + final FragmentManagerImpl mManager; + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REPLACE = 2; + static final int OP_REMOVE = 3; + static final int OP_HIDE = 4; + static final int OP_SHOW = 5; + static final int OP_DETACH = 6; + static final int OP_ATTACH = 7; + + static final class Op { + Op next; + Op prev; + int cmd; + Fragment fragment; + int enterAnim; + int exitAnim; + int popEnterAnim; + int popExitAnim; + ArrayList removed; + } + + Op mHead; + Op mTail; + int mNumOp; + int mEnterAnim; + int mExitAnim; + int mPopEnterAnim; + int mPopExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + boolean mAllowAddToBackStack = true; + String mName; + boolean mCommitted; + int mIndex = -1; + + int mBreadCrumbTitleRes; + CharSequence mBreadCrumbTitleText; + int mBreadCrumbShortTitleRes; + CharSequence mBreadCrumbShortTitleText; + + ArrayList mSharedElementSourceNames; + ArrayList mSharedElementTargetNames; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("BackStackEntry{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mName != null) { + sb.append(" "); + sb.append(mName); + } + sb.append("}"); + return sb.toString(); + } + + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + dump(prefix, writer, true); + } + + public void dump(String prefix, PrintWriter writer, boolean full) { + if (full) { + writer.print(prefix); writer.print("mName="); writer.print(mName); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mCommitted="); writer.println(mCommitted); + if (mTransition != FragmentTransaction.TRANSIT_NONE) { + writer.print(prefix); writer.print("mTransition=#"); + writer.print(Integer.toHexString(mTransition)); + writer.print(" mTransitionStyle=#"); + writer.println(Integer.toHexString(mTransitionStyle)); + } + if (mEnterAnim != 0 || mExitAnim !=0) { + writer.print(prefix); writer.print("mEnterAnim=#"); + writer.print(Integer.toHexString(mEnterAnim)); + writer.print(" mExitAnim=#"); + writer.println(Integer.toHexString(mExitAnim)); + } + if (mPopEnterAnim != 0 || mPopExitAnim !=0) { + writer.print(prefix); writer.print("mPopEnterAnim=#"); + writer.print(Integer.toHexString(mPopEnterAnim)); + writer.print(" mPopExitAnim=#"); + writer.println(Integer.toHexString(mPopExitAnim)); + } + if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbTitleRes)); + writer.print(" mBreadCrumbTitleText="); + writer.println(mBreadCrumbTitleText); + } + if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbShortTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbShortTitleRes)); + writer.print(" mBreadCrumbShortTitleText="); + writer.println(mBreadCrumbShortTitleText); + } + } + + if (mHead != null) { + writer.print(prefix); writer.println("Operations:"); + String innerPrefix = prefix + " "; + Op op = mHead; + int num = 0; + while (op != null) { + String cmdStr; + switch (op.cmd) { + case OP_NULL: cmdStr="NULL"; break; + case OP_ADD: cmdStr="ADD"; break; + case OP_REPLACE: cmdStr="REPLACE"; break; + case OP_REMOVE: cmdStr="REMOVE"; break; + case OP_HIDE: cmdStr="HIDE"; break; + case OP_SHOW: cmdStr="SHOW"; break; + case OP_DETACH: cmdStr="DETACH"; break; + case OP_ATTACH: cmdStr="ATTACH"; break; + default: cmdStr="cmd=" + op.cmd; break; + } + writer.print(prefix); writer.print(" Op #"); writer.print(num); + writer.print(": "); writer.print(cmdStr); + writer.print(" "); writer.println(op.fragment); + if (full) { + if (op.enterAnim != 0 || op.exitAnim != 0) { + writer.print(prefix); writer.print("enterAnim=#"); + writer.print(Integer.toHexString(op.enterAnim)); + writer.print(" exitAnim=#"); + writer.println(Integer.toHexString(op.exitAnim)); + } + if (op.popEnterAnim != 0 || op.popExitAnim != 0) { + writer.print(prefix); writer.print("popEnterAnim=#"); + writer.print(Integer.toHexString(op.popEnterAnim)); + writer.print(" popExitAnim=#"); + writer.println(Integer.toHexString(op.popExitAnim)); + } + } + if (op.removed != null && op.removed.size() > 0) { + for (int i=0; i(); + mSharedElementTargetNames = new ArrayList(); + } + + mSharedElementSourceNames.add(transitionName); + mSharedElementTargetNames.add(name); + } + return this; + } + + public FragmentTransaction setTransitionStyle(int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + public FragmentTransaction addToBackStack(String name) { + if (!mAllowAddToBackStack) { + throw new IllegalStateException( + "This FragmentTransaction is not allowed to be added to the back stack."); + } + mAddToBackStack = true; + mName = name; + return this; + } + + public boolean isAddToBackStackAllowed() { + return mAllowAddToBackStack; + } + + public FragmentTransaction disallowAddToBackStack() { + if (mAddToBackStack) { + throw new IllegalStateException( + "This transaction is already being added to the back stack"); + } + mAllowAddToBackStack = false; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(int res) { + mBreadCrumbTitleRes = res; + mBreadCrumbTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(CharSequence text) { + mBreadCrumbTitleRes = 0; + mBreadCrumbTitleText = text; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(int res) { + mBreadCrumbShortTitleRes = res; + mBreadCrumbShortTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(CharSequence text) { + mBreadCrumbShortTitleRes = 0; + mBreadCrumbShortTitleText = text; + return this; + } + + void bumpBackStackNesting(int amt) { + if (!mAddToBackStack) { + return; + } + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting in " + this + + " by " + amt); + Op op = mHead; + while (op != null) { + if (op.fragment != null) { + op.fragment.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + op.fragment + " to " + op.fragment.mBackStackNesting); + } + if (op.removed != null) { + for (int i=op.removed.size()-1; i>=0; i--) { + Fragment r = op.removed.get(i); + r.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + r + " to " + r.mBackStackNesting); + } + } + op = op.next; + } + } + + public int commit() { + return commitInternal(false); + } + + public int commitAllowingStateLoss() { + return commitInternal(true); + } + + int commitInternal(boolean allowStateLoss) { + if (mCommitted) throw new IllegalStateException("commit already called"); + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Commit: " + this); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + dump(" ", null, pw, null); + } + mCommitted = true; + if (mAddToBackStack) { + mIndex = mManager.allocBackStackIndex(this); + } else { + mIndex = -1; + } + mManager.enqueueAction(this, allowStateLoss); + return mIndex; + } + + public void run() { + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Run: " + this); + + if (mAddToBackStack) { + if (mIndex < 0) { + throw new IllegalStateException("addToBackStack() called after commit()"); + } + } + + bumpBackStackNesting(1); + + TransitionState state = null; + SparseArray firstOutFragments = null; + SparseArray lastInFragments = null; + if (SUPPORTS_TRANSITIONS) { + firstOutFragments = new SparseArray(); + lastInFragments = new SparseArray(); + + calculateFragments(firstOutFragments, lastInFragments); + + state = beginTransition(firstOutFragments, lastInFragments, false); + } + + int transitionStyle = state != null ? 0 : mTransitionStyle; + int transition = state != null ? 0 : mTransition; + Op op = mHead; + while (op != null) { + int enterAnim = state != null ? 0 : op.enterAnim; + int exitAnim = state != null ? 0 : op.exitAnim; + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + f.mNextAnim = enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i=0; i fragments, Fragment fragment) { + if (fragment != null) { + int containerId = fragment.mContainerId; + if (containerId != 0 && !fragment.isHidden() && fragment.isAdded() && + fragment.getView() != null && fragments.get(containerId) == null) { + fragments.put(containerId, fragment); + } + } + } + + private void setLastIn(SparseArray fragments, Fragment fragment) { + if (fragment != null) { + int containerId = fragment.mContainerId; + if (containerId != 0) { + fragments.put(containerId, fragment); + } + } + } + + /** + * Finds the first removed fragment and last added fragments when going forward. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. This list will be modified by the method. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. This list will be modified by the method. + */ + private void calculateFragments(SparseArray firstOutFragments, + SparseArray lastInFragments) { + if (!mManager.mContainer.hasView()) { + return; // nothing to see, so no transitions + } + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: + setLastIn(lastInFragments, op.fragment); + break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i = 0; i < mManager.mAdded.size(); i++) { + Fragment old = mManager.mAdded.get(i); + if (f == null || old.mContainerId == f.mContainerId) { + if (old == f) { + f = null; + } else { + setFirstOut(firstOutFragments, old); + } + } + } + } + setLastIn(lastInFragments, f); + break; + } + case OP_REMOVE: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_HIDE: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_SHOW: + setLastIn(lastInFragments, op.fragment); + break; + case OP_DETACH: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_ATTACH: + setLastIn(lastInFragments, op.fragment); + break; + } + + op = op.next; + } + } + + /** + * Finds the first removed fragment and last added fragments when popping the back stack. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. This list will be modified by the method. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. This list will be modified by the method. + */ + public void calculateBackFragments(SparseArray firstOutFragments, + SparseArray lastInFragments) { + if (!mManager.mContainer.hasView()) { + return; // nothing to see, so no transitions + } + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_REPLACE: + if (op.removed != null) { + for (int i = op.removed.size() - 1; i >= 0; i--) { + setLastIn(lastInFragments, op.removed.get(i)); + } + } + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_REMOVE: + setLastIn(lastInFragments, op.fragment); + break; + case OP_HIDE: + setLastIn(lastInFragments, op.fragment); + break; + case OP_SHOW: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_DETACH: + setLastIn(lastInFragments, op.fragment); + break; + case OP_ATTACH: + setFirstOut(firstOutFragments, op.fragment); + break; + } + + op = op.next; + } + } + + public TransitionState popFromBackStack(boolean doStateMove, TransitionState state, + SparseArray firstOutFragments, SparseArray lastInFragments) { + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "popFromBackStack: " + this); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + dump(" ", null, pw, null); + } + + if (SUPPORTS_TRANSITIONS) { + if (state == null) { + if (firstOutFragments.size() != 0 || lastInFragments.size() != 0) { + state = beginTransition(firstOutFragments, lastInFragments, true); + } + } else if (!doStateMove) { + setNameOverrides(state, mSharedElementTargetNames, mSharedElementSourceNames); + } + } + + bumpBackStackNesting(-1); + + int transitionStyle = state != null ? 0 : mTransitionStyle; + int transition = state != null ? 0 : mTransition; + Op op = mTail; + while (op != null) { + int popEnterAnim = state != null ? 0 : op.popEnterAnim; + int popExitAnim= state != null ? 0 : op.popExitAnim; + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + f.mNextAnim = popExitAnim; + mManager.removeFragment(f, + FragmentManagerImpl.reverseTransit(transition), transitionStyle); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (f != null) { + f.mNextAnim = popExitAnim; + mManager.removeFragment(f, + FragmentManagerImpl.reverseTransit(transition), transitionStyle); + } + if (op.removed != null) { + for (int i=0; i= 0) { + mManager.freeBackStackIndex(mIndex); + mIndex = -1; + } + return state; + } + + public String getName() { + return mName; + } + + public int getTransition() { + return mTransition; + } + + public int getTransitionStyle() { + return mTransitionStyle; + } + + public boolean isEmpty() { + return mNumOp == 0; + } + + /** + * When custom fragment transitions are used, this sets up the state for each transition + * and begins the transition. A different transition is started for each fragment container + * and consists of up to 3 different transitions: the exit transition, a shared element + * transition and an enter transition. + * + *

The exit transition operates against the leaf nodes of the first fragment + * with a view that was removed. If no such fragment was removed, then no exit + * transition is executed. The exit transition comes from the outgoing fragment.

+ * + *

The enter transition operates against the last fragment that was added. If + * that fragment does not have a view or no fragment was added, then no enter + * transition is executed. The enter transition comes from the incoming fragment.

+ * + *

The shared element transition operates against all views and comes either + * from the outgoing fragment or the incoming fragment, depending on whether this + * is going forward or popping the back stack. When going forward, the incoming + * fragment's enter shared element transition is used, but when going back, the + * outgoing fragment's return shared element transition is used. Shared element + * transitions only operate if there is both an incoming and outgoing fragment.

+ * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + * @return The TransitionState used to complete the operation of the transition + * in {@link #setNameOverrides(BackStackRecord.TransitionState, java.util.ArrayList, + * java.util.ArrayList)}. + */ + private TransitionState beginTransition(SparseArray firstOutFragments, + SparseArray lastInFragments, boolean isBack) { + TransitionState state = new TransitionState(); + + // Adding a non-existent target view makes sure that the transitions don't target + // any views by default. They'll only target the views we tell add. If we don't + // add any, then no views will be targeted. + state.nonExistentView = new View(mManager.mActivity); + + boolean anyTransitionStarted = false; + // Go over all leaving fragments. + for (int i = 0; i < firstOutFragments.size(); i++) { + int containerId = firstOutFragments.keyAt(i); + if (configureTransitions(containerId, state, isBack, firstOutFragments, + lastInFragments)) { + anyTransitionStarted = true; + } + } + + // Now go over all entering fragments that didn't have a leaving fragment. + for (int i = 0; i < lastInFragments.size(); i++) { + int containerId = lastInFragments.keyAt(i); + if (firstOutFragments.get(containerId) == null && + configureTransitions(containerId, state, isBack, firstOutFragments, + lastInFragments)) { + anyTransitionStarted = true; + } + } + + if (!anyTransitionStarted) { + state = null; + } + + return state; + } + + private static Object getEnterTransition(Fragment inFragment, boolean isBack) { + if (inFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + inFragment.getReenterTransition() : inFragment.getEnterTransition()); + } + + private static Object getExitTransition(Fragment outFragment, boolean isBack) { + if (outFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + outFragment.getReturnTransition() : outFragment.getExitTransition()); + } + + private static Object getSharedElementTransition(Fragment inFragment, Fragment outFragment, + boolean isBack) { + if (inFragment == null || outFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + outFragment.getSharedElementReturnTransition() : + inFragment.getSharedElementEnterTransition()); + } + + private static Object captureExitingViews(Object exitTransition, Fragment outFragment, + ArrayList exitingViews, ArrayMap namedViews, View nonExistentView) { + if (exitTransition != null) { + exitTransition = FragmentTransitionCompat21.captureExitingViews(exitTransition, + outFragment.getView(), exitingViews, namedViews, nonExistentView); + } + return exitTransition; + } + + private ArrayMap remapSharedElements(TransitionState state, Fragment outFragment, + boolean isBack) { + ArrayMap namedViews = new ArrayMap(); + if (mSharedElementSourceNames != null) { + FragmentTransitionCompat21.findNamedViews(namedViews, outFragment.getView()); + if (isBack) { + namedViews.retainAll(mSharedElementTargetNames); + } else { + namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, + namedViews); + } + } + + if (isBack) { + if (outFragment.mEnterTransitionCallback != null) { + outFragment.mEnterTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setBackNameOverrides(state, namedViews, false); + } else { + if (outFragment.mExitTransitionCallback != null) { + outFragment.mExitTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setNameOverrides(state, namedViews, false); + } + + return namedViews; + } + + /** + * Configures custom transitions for a specific fragment container. + * + * @param containerId The container ID of the fragments to configure the transition for. + * @param state The Transition State keeping track of the executing transitions. + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + */ + private boolean configureTransitions(int containerId, TransitionState state, boolean isBack, + SparseArray firstOutFragments, SparseArray lastInFragments) { + ViewGroup sceneRoot = (ViewGroup) mManager.mContainer.findViewById(containerId); + if (sceneRoot == null) { + return false; + } + final Fragment inFragment = lastInFragments.get(containerId); + Fragment outFragment = firstOutFragments.get(containerId); + + Object enterTransition = getEnterTransition(inFragment, isBack); + Object sharedElementTransition = getSharedElementTransition(inFragment, outFragment, + isBack); + Object exitTransition = getExitTransition(outFragment, isBack); + if (enterTransition == null && sharedElementTransition == null && + exitTransition == null) { + return false; // no transitions! + } + ArrayMap namedViews = null; + ArrayList sharedElementTargets = new ArrayList(); + if (sharedElementTransition != null) { + namedViews = remapSharedElements(state, outFragment, isBack); + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); + + // Notify the start of the transition. + SharedElementCallback callback = isBack ? + outFragment.mEnterTransitionCallback : + inFragment.mEnterTransitionCallback; + if (callback != null) { + ArrayList names = new ArrayList(namedViews.keySet()); + ArrayList views = new ArrayList(namedViews.values()); + callback.onSharedElementStart(names, views, null); + } + } + + ArrayList exitingViews = new ArrayList(); + exitTransition = captureExitingViews(exitTransition, outFragment, exitingViews, + namedViews, state.nonExistentView); + + // Set the epicenter of the exit transition + if (mSharedElementTargetNames != null && namedViews != null) { + View epicenterView = namedViews.get(mSharedElementTargetNames.get(0)); + if (epicenterView != null) { + if (exitTransition != null) { + FragmentTransitionCompat21.setEpicenter(exitTransition, epicenterView); + } + if (sharedElementTransition != null) { + FragmentTransitionCompat21.setEpicenter(sharedElementTransition, + epicenterView); + } + } + } + + FragmentTransitionCompat21.ViewRetriever viewRetriever = + new FragmentTransitionCompat21.ViewRetriever() { + @Override + public View getView() { + return inFragment.getView(); + } + }; + + if (sharedElementTransition != null) { + prepareSharedElementTransition(state, sceneRoot, sharedElementTransition, + inFragment, outFragment, isBack, sharedElementTargets); + } + + ArrayList enteringViews = new ArrayList(); + ArrayMap renamedViews = new ArrayMap(); + + boolean allowOverlap = isBack ? inFragment.getAllowReturnTransitionOverlap() : + inFragment.getAllowEnterTransitionOverlap(); + Object transition = FragmentTransitionCompat21.mergeTransitions(enterTransition, + exitTransition, sharedElementTransition, allowOverlap); + + if (transition != null) { + FragmentTransitionCompat21.addTransitionTargets(enterTransition, + sharedElementTransition, sceneRoot, viewRetriever, state.nonExistentView, + state.enteringEpicenterView, state.nameOverrides, enteringViews, + renamedViews, sharedElementTargets); + excludeHiddenFragmentsAfterEnter(sceneRoot, state, containerId, transition); + + // We want to exclude hidden views later, so we need a non-null list in the + // transition now. + FragmentTransitionCompat21.excludeTarget(transition, state.nonExistentView, true); + // Now exclude all currently hidden fragments. + excludeHiddenFragments(state, containerId, transition); + + FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition); + + FragmentTransitionCompat21.cleanupTransitions(sceneRoot, state.nonExistentView, + enterTransition, enteringViews, exitTransition, exitingViews, + sharedElementTransition, sharedElementTargets, + transition, state.hiddenFragmentViews, renamedViews); + } + return transition != null; + } + + private void prepareSharedElementTransition(final TransitionState state, final View sceneRoot, + final Object sharedElementTransition, final Fragment inFragment, + final Fragment outFragment, final boolean isBack, + final ArrayList sharedElementTargets) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + + if (sharedElementTransition != null) { + FragmentTransitionCompat21.removeTargets(sharedElementTransition, + sharedElementTargets); + sharedElementTargets.clear(); + + ArrayMap namedViews = mapSharedElementsIn( + state, isBack, inFragment); + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); + FragmentTransitionCompat21.addTargets(sharedElementTransition, + sharedElementTargets); + + setEpicenterIn(namedViews, state); + + callSharedElementEnd(state, inFragment, outFragment, isBack, + namedViews); + } + + return true; + } + }); + } + + private void callSharedElementEnd(TransitionState state, Fragment inFragment, + Fragment outFragment, boolean isBack, ArrayMap namedViews) { + SharedElementCallback sharedElementCallback = isBack ? + outFragment.mEnterTransitionCallback : + inFragment.mEnterTransitionCallback; + if (sharedElementCallback != null) { + ArrayList names = new ArrayList(namedViews.keySet()); + ArrayList views = new ArrayList(namedViews.values()); + sharedElementCallback.onSharedElementEnd(names, views, null); + } + } + + private void setEpicenterIn(ArrayMap namedViews, TransitionState state) { + if (mSharedElementTargetNames != null && !namedViews.isEmpty()) { + // now we know the epicenter of the entering transition. + View epicenter = namedViews + .get(mSharedElementTargetNames.get(0)); + if (epicenter != null) { + state.enteringEpicenterView.epicenter = epicenter; + } + } + } + + private ArrayMap mapSharedElementsIn(TransitionState state, + boolean isBack, Fragment inFragment) { + // Now map the shared elements in the incoming fragment + ArrayMap namedViews = mapEnteringSharedElements(state, inFragment, isBack); + + // remap shared elements and set the name mapping used + // in the shared element transition. + if (isBack) { + if (inFragment.mExitTransitionCallback != null) { + inFragment.mExitTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setBackNameOverrides(state, namedViews, true); + } else { + if (inFragment.mEnterTransitionCallback != null) { + inFragment.mEnterTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setNameOverrides(state, namedViews, true); + } + return namedViews; + } + + /** + * Remaps a name-to-View map, substituting different names for keys. + * + * @param inMap A list of keys found in the map, in the order in toGoInMap + * @param toGoInMap A list of keys to use for the new map, in the order of inMap + * @param namedViews The current mapping + * @return A copy of namedViews with the keys coming from toGoInMap. + */ + private static ArrayMap remapNames(ArrayList inMap, + ArrayList toGoInMap, ArrayMap namedViews) { + if (namedViews.isEmpty()) { + return namedViews; + } + ArrayMap remappedViews = new ArrayMap(); + int numKeys = inMap.size(); + for (int i = 0; i < numKeys; i++) { + View view = namedViews.get(inMap.get(i)); + if (view != null) { + remappedViews.put(toGoInMap.get(i), view); + } + } + return remappedViews; + } + + /** + * Maps shared elements to views in the entering fragment. + * + * @param state The transition State as returned from {@link #beginTransition( + * android.util.SparseArray, android.util.SparseArray, boolean)}. + * @param inFragment The last fragment to be added. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + */ + private ArrayMap mapEnteringSharedElements(TransitionState state, + Fragment inFragment, boolean isBack) { + ArrayMap namedViews = new ArrayMap(); + View root = inFragment.getView(); + if (root != null) { + if (mSharedElementSourceNames != null) { + FragmentTransitionCompat21.findNamedViews(namedViews, root); + if (isBack) { + namedViews = remapNames(mSharedElementSourceNames, + mSharedElementTargetNames, namedViews); + } else { + namedViews.retainAll(mSharedElementTargetNames); + } + } + } + return namedViews; + } + + private void excludeHiddenFragmentsAfterEnter(final View sceneRoot, final TransitionState state, + final int containerId, final Object transition) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + excludeHiddenFragments(state, containerId, transition); + return true; + } + }); + } + + private void excludeHiddenFragments(TransitionState state, int containerId, Object transition) { + if (mManager.mAdded != null) { + for (int i = 0; i < mManager.mAdded.size(); i++) { + Fragment fragment = mManager.mAdded.get(i); + if (fragment.mView != null && fragment.mContainer != null && + fragment.mContainerId == containerId) { + if (fragment.mHidden) { + if (!state.hiddenFragmentViews.contains(fragment.mView)) { + FragmentTransitionCompat21.excludeTarget(transition, fragment.mView, + true); + state.hiddenFragmentViews.add(fragment.mView); + } + } else { + FragmentTransitionCompat21.excludeTarget(transition, fragment.mView, + false); + state.hiddenFragmentViews.remove(fragment.mView); + } + } + } + } + } + + private static void setNameOverride(ArrayMap overrides, + String source, String target) { + if (source != null && target != null && !source.equals(target)) { + for (int index = 0; index < overrides.size(); index++) { + if (source.equals(overrides.valueAt(index))) { + overrides.setValueAt(index, target); + return; + } + } + overrides.put(source, target); + } + } + + private static void setNameOverrides(TransitionState state, ArrayList sourceNames, + ArrayList targetNames) { + if (sourceNames != null) { + for (int i = 0; i < sourceNames.size(); i++) { + String source = sourceNames.get(i); + String target = targetNames.get(i); + setNameOverride(state.nameOverrides, source, target); + } + } + } + + private void setBackNameOverrides(TransitionState state, ArrayMap namedViews, + boolean isEnd) { + int count = mSharedElementTargetNames == null ? 0 : mSharedElementTargetNames.size(); + for (int i = 0; i < count; i++) { + String source = mSharedElementSourceNames.get(i); + String originalTarget = mSharedElementTargetNames.get(i); + View view = namedViews.get(originalTarget); + if (view != null) { + String target = FragmentTransitionCompat21.getTransitionName(view); + if (isEnd) { + setNameOverride(state.nameOverrides, source, target); + } else { + setNameOverride(state.nameOverrides, target, source); + } + } + } + } + + private void setNameOverrides(TransitionState state, ArrayMap namedViews, + boolean isEnd) { + int count = namedViews.size(); + for (int i = 0; i < count; i++) { + String source = namedViews.keyAt(i); + String target = FragmentTransitionCompat21.getTransitionName(namedViews.valueAt(i)); + if (isEnd) { + setNameOverride(state.nameOverrides, source, target); + } else { + setNameOverride(state.nameOverrides, target, source); + } + } + } + + public class TransitionState { + public ArrayMap nameOverrides = new ArrayMap(); + public ArrayList hiddenFragmentViews = new ArrayList(); + + public FragmentTransitionCompat21.EpicenterView enteringEpicenterView = + new FragmentTransitionCompat21.EpicenterView(); + public View nonExistentView; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java b/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java new file mode 100644 index 00000000000..fb6350d96b0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java @@ -0,0 +1,27 @@ +package android.support.v4.app; + +import android.os.Bundle; +import android.os.Parcelable; + +import java.util.Arrays; + +/** + * @hide + */ +class BundleUtil { + /** + * Get an array of Bundle objects from a parcelable array field in a bundle. + * Update the bundle to have a typed array so fetches in the future don't need + * to do an array copy. + */ + public static Bundle[] getBundleArrayFromBundle(Bundle bundle, String key) { + Parcelable[] array = bundle.getParcelableArray(key); + if (array instanceof Bundle[] || array == null) { + return (Bundle[]) array; + } + Bundle[] typedArray = Arrays.copyOf(array, array.length, + Bundle[].class); + bundle.putParcelableArray(key, typedArray); + return typedArray; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java new file mode 100644 index 00000000000..343c1454781 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StyleRes; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Static library support version of the framework's {@link android.app.DialogFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class DialogFragment extends Fragment + implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + /** @hide */ + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + /** + * Style for {@link #setStyle(int, int)}: a basic, + * normal dialog. + */ + public static final int STYLE_NORMAL = 0; + + /** + * Style for {@link #setStyle(int, int)}: don't include + * a title area. + */ + public static final int STYLE_NO_TITLE = 1; + + /** + * Style for {@link #setStyle(int, int)}: don't draw + * any frame at all; the view hierarchy returned by {@link #onCreateView} + * is entirely responsible for drawing the dialog. + */ + public static final int STYLE_NO_FRAME = 2; + + /** + * Style for {@link #setStyle(int, int)}: like + * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. + * The user can not touch it, and its window will not receive input focus. + */ + public static final int STYLE_NO_INPUT = 3; + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + private static final String SAVED_STYLE = "android:style"; + private static final String SAVED_THEME = "android:theme"; + private static final String SAVED_CANCELABLE = "android:cancelable"; + private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; + private static final String SAVED_BACK_STACK_ID = "android:backStackId"; + + int mStyle = STYLE_NORMAL; + int mTheme = 0; + boolean mCancelable = true; + boolean mShowsDialog = true; + int mBackStackId = -1; + + Dialog mDialog; + boolean mViewDestroyed; + boolean mDismissed; + boolean mShownByMe; + + public DialogFragment() { + } + + /** + * Call to customize the basic appearance and behavior of the + * fragment's dialog. This can be used for some common dialog behaviors, + * taking care of selecting flags, theme, and other options for you. The + * same effect can be achieve by manually setting Dialog and Window + * attributes yourself. Calling this after the fragment's Dialog is + * created will have no effect. + * + * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, + * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or + * {@link #STYLE_NO_INPUT}. + * @param theme Optional custom theme. If 0, an appropriate theme (based + * on the style) will be selected for you. + */ + public void setStyle(@DialogStyle int style, @StyleRes int theme) { + mStyle = style; + if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { + mTheme = android.R.style.Theme_Panel; + } + if (theme != 0) { + mTheme = theme; + } + } + + /** + * Display the dialog, adding the fragment to the given FragmentManager. This + * is a convenience for explicitly creating a transaction, adding the + * fragment to it with the given tag, and committing it. This does + * not add the transaction to the back stack. When the fragment + * is dismissed, a new transaction will be executed to remove it from + * the activity. + * @param manager The FragmentManager this fragment will be added to. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + */ + public void show(FragmentManager manager, String tag) { + mDismissed = false; + mShownByMe = true; + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commit(); + } + + /** + * Display the dialog, adding the fragment using an existing transaction + * and then committing the transaction. + * @param transaction An existing transaction in which to add the fragment. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + * @return Returns the identifier of the committed transaction, as per + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. + */ + public int show(FragmentTransaction transaction, String tag) { + mDismissed = false; + mShownByMe = true; + transaction.add(this, tag); + mViewDestroyed = false; + mBackStackId = transaction.commit(); + return mBackStackId; + } + + /** + * Dismiss the fragment and its dialog. If the fragment was added to the + * back stack, all back stack state up to and including this entry will + * be popped. Otherwise, a new transaction will be committed to remove + * the fragment. + */ + public void dismiss() { + dismissInternal(false); + } + + /** + * Version of {@link #dismiss()} that uses + * {@link FragmentTransaction#commitAllowingStateLoss() + * FragmentTransaction.commitAllowingStateLoss()}. See linked + * documentation for further details. + */ + public void dismissAllowingStateLoss() { + dismissInternal(true); + } + + void dismissInternal(boolean allowStateLoss) { + if (mDismissed) { + return; + } + mDismissed = true; + mShownByMe = false; + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + mViewDestroyed = true; + if (mBackStackId >= 0) { + getFragmentManager().popBackStack(mBackStackId, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackId = -1; + } else { + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.remove(this); + if (allowStateLoss) { + ft.commitAllowingStateLoss(); + } else { + ft.commit(); + } + } + } + + public Dialog getDialog() { + return mDialog; + } + + @StyleRes + public int getTheme() { + return mTheme; + } + + /** + * Control whether the shown Dialog is cancelable. Use this instead of + * directly calling {@link Dialog#setCancelable(boolean) + * Dialog.setCancelable(boolean)}, because DialogFragment needs to change + * its behavior based on this. + * + * @param cancelable If true, the dialog is cancelable. The default + * is true. + */ + public void setCancelable(boolean cancelable) { + mCancelable = cancelable; + if (mDialog != null) mDialog.setCancelable(cancelable); + } + + /** + * Return the current value of {@link #setCancelable(boolean)}. + */ + public boolean isCancelable() { + return mCancelable; + } + + /** + * Controls whether this fragment should be shown in a dialog. If not + * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, + * and the fragment's view hierarchy will thus not be added to it. This + * allows you to instead use it as a normal fragment (embedded inside of + * its activity). + * + *

This is normally set for you based on whether the fragment is + * associated with a container view ID passed to + * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. + * If the fragment was added with a container, setShowsDialog will be + * initialized to false; otherwise, it will be true. + * + * @param showsDialog If true, the fragment will be displayed in a Dialog. + * If false, no Dialog will be created and the fragment's view hierarchly + * left undisturbed. + */ + public void setShowsDialog(boolean showsDialog) { + mShowsDialog = showsDialog; + } + + /** + * Return the current value of {@link #setShowsDialog(boolean)}. + */ + public boolean getShowsDialog() { + return mShowsDialog; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!mShownByMe) { + // If not explicitly shown through our API, take this as an + // indication that the dialog is no longer dismissed. + mDismissed = false; + } + } + + @Override + public void onDetach() { + super.onDetach(); + if (!mShownByMe && !mDismissed) { + // The fragment was not shown by a direct call here, it is not + // dismissed, and now it is being detached... well, okay, thou + // art now dismissed. Have fun. + mDismissed = true; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mShowsDialog = mContainerId == 0; + + if (savedInstanceState != null) { + mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); + mTheme = savedInstanceState.getInt(SAVED_THEME, 0); + mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); + mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); + } + + } + + /** @hide */ + @Override + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + if (!mShowsDialog) { + return super.getLayoutInflater(savedInstanceState); + } + + mDialog = onCreateDialog(savedInstanceState); + switch (mStyle) { + case STYLE_NO_INPUT: + mDialog.getWindow().addFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + // fall through... + case STYLE_NO_FRAME: + case STYLE_NO_TITLE: + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + } + if (mDialog != null) { + return (LayoutInflater) mDialog.getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + return (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Override to build your own custom Dialog container. This is typically + * used to show an AlertDialog instead of a generic Dialog; when doing so, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need + * to be implemented since the AlertDialog takes care of its own content. + * + *

This method will be called after {@link #onCreate(Bundle)} and + * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The + * default implementation simply instantiates and returns a {@link Dialog} + * class. + * + *

Note: DialogFragment own the {@link Dialog#setOnCancelListener + * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener + * Dialog.setOnDismissListener} callbacks. You must not set them yourself. + * To find out about these events, override {@link #onCancel(DialogInterface)} + * and {@link #onDismiss(DialogInterface)}.

+ * + * @param savedInstanceState The last saved instance state of the Fragment, + * or null if this is a freshly created Fragment. + * + * @return Return a new Dialog instance to be displayed by the Fragment. + */ + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new Dialog(getActivity(), getTheme()); + } + + public void onCancel(DialogInterface dialog) { + } + + public void onDismiss(DialogInterface dialog) { + if (!mViewDestroyed) { + // Note: we need to use allowStateLoss, because the dialog + // dispatches this asynchronously so we can receive the call + // after the activity is paused. Worst case, when the user comes + // back to the activity they see the dialog again. + dismissInternal(true); + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (!mShowsDialog) { + return; + } + + View view = getView(); + if (view != null) { + if (view.getParent() != null) { + throw new IllegalStateException("DialogFragment can not be attached to a container view"); + } + mDialog.setContentView(view); + } + mDialog.setOwnerActivity(getActivity()); + mDialog.setCancelable(mCancelable); + mDialog.setOnCancelListener(this); + mDialog.setOnDismissListener(this); + if (savedInstanceState != null) { + Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + mDialog.onRestoreInstanceState(dialogState); + } + } + } + + @Override + public void onStart() { + super.onStart(); + if (mDialog != null) { + mViewDestroyed = false; + mDialog.show(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mDialog != null) { + Bundle dialogState = mDialog.onSaveInstanceState(); + if (dialogState != null) { + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + } + if (mStyle != STYLE_NORMAL) { + outState.putInt(SAVED_STYLE, mStyle); + } + if (mTheme != 0) { + outState.putInt(SAVED_THEME, mTheme); + } + if (!mCancelable) { + outState.putBoolean(SAVED_CANCELABLE, mCancelable); + } + if (!mShowsDialog) { + outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + } + if (mBackStackId != -1) { + outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mDialog != null) { + mDialog.hide(); + } + } + + /** + * Remove dialog. + */ + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mDialog != null) { + // Set removed here because this dismissal is just to hide + // the dialog -- we don't want this to cause the fragment to + // actually be removed. + mViewDestroyed = true; + mDialog.dismiss(); + mDialog = null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java new file mode 100644 index 00000000000..eb6ab0d3fad --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java @@ -0,0 +1,2015 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.util.SimpleArrayMap; +import android.support.v4.util.DebugUtils; +import android.support.v4.view.LayoutInflaterCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; +import android.view.animation.Animation; +import android.widget.AdapterView; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +final class FragmentState implements Parcelable { + final String mClassName; + final int mIndex; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + final boolean mDetached; + final Bundle mArguments; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + public FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mIndex = frag.mIndex; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + mDetached = frag.mDetached; + mArguments = frag.mArguments; + } + + public FragmentState(Parcel in) { + mClassName = in.readString(); + mIndex = in.readInt(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mDetached = in.readInt() != 0; + mArguments = in.readBundle(); + mSavedFragmentState = in.readBundle(); + } + + public Fragment instantiate(FragmentActivity activity, Fragment parent) { + if (mInstance != null) { + return mInstance; + } + + if (mArguments != null) { + mArguments.setClassLoader(activity.getClassLoader()); + } + + mInstance = Fragment.instantiate(activity, mClassName, mArguments); + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(activity.getClassLoader()); + mInstance.mSavedFragmentState = mSavedFragmentState; + } + mInstance.setIndex(mIndex, parent); + mInstance.mFromLayout = mFromLayout; + mInstance.mRestored = true; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + mInstance.mDetached = mDetached; + mInstance.mFragmentManager = activity.mFragments; + + if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG, + "Instantiated fragment " + mInstance); + + return mInstance; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeInt(mIndex); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeInt(mDetached ? 1 : 0); + dest.writeBundle(mArguments); + dest.writeBundle(mSavedFragmentState); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} + +/** + * Static library support version of the framework's {@link android.app.Fragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link android.app.Fragment} + * documentation for a class overview. + * + *

The main differences when using this support version instead of the framework version are: + *

    + *
  • Your activity must extend {@link FragmentActivity} + *
  • You must call {@link FragmentActivity#getSupportFragmentManager} to get the + * {@link FragmentManager} + *
+ * + */ +public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener { + private static final SimpleArrayMap> sClassMap = + new SimpleArrayMap>(); + + static final Object USE_DEFAULT_TRANSITION = new Object(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. + static final int STOPPED = 3; // Fully created, not started. + static final int STARTED = 4; // Created and started, not resumed. + static final int RESUMED = 5; // Created started and resumed. + + int mState = INITIALIZING; + + // Non-null if the fragment's view hierarchy is currently animating away, + // meaning we need to wait a bit on completely destroying it. This is the + // view that is animating. + View mAnimatingAway; + + // If mAnimatingAway != null, this is the state we should move to once the + // animation is done. + int mStateAfterAnimating; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray mSavedViewState; + + // Index into active fragment array. + int mIndex = -1; + + // Internal unique name for this fragment; + String mWho; + + // Construction arguments; + Bundle mArguments; + + // Target fragment. + Fragment mTarget; + + // For use when retaining a fragment: this is the index of the last mTarget. + int mTargetIndex = -1; + + // Target request code. + int mTargetRequestCode; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // If set this fragment is being removed from its activity. + boolean mRemoving; + + // True if the fragment is in the resumed state. + boolean mResumed; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Set to true when the view has actually been inflated in its layout. + boolean mInLayout; + + // True if this fragment has been restored from previously saved state. + boolean mRestored; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // The fragment manager we are associated with. Set as soon as the + // fragment is used in a transaction; cleared after it has been removed + // from all transactions. + FragmentManagerImpl mFragmentManager; + + // Activity this fragment is attached to. + FragmentActivity mActivity; + + // Private fragment manager for child fragments inside of this one. + FragmentManagerImpl mChildFragmentManager; + + // If this Fragment is contained in another Fragment, this is that container. + Fragment mParentFragment; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // Set to true when the app has requested that this fragment be deactivated. + boolean mDetached; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment is being retained across the current config change. + boolean mRetaining; + + // If set this fragment has menu items to contribute. + boolean mHasMenu; + + // Set to true to allow the fragment's menu to be shown. + boolean mMenuVisible = true; + + // Used to verify that subclasses call through to super class. + boolean mCalled; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + // The real inner view that will save/restore state. + View mInnerView; + + // Whether this fragment should defer starting until after other fragments + // have been started and their loaders are finished. + boolean mDeferStart; + + // Hint provided by the app that this fragment is currently visible to the user. + boolean mUserVisibleHint = true; + + LoaderManagerImpl mLoaderManager; + boolean mLoadersStarted; + boolean mCheckedForLoaderManager; + + Object mEnterTransition = null; + Object mReturnTransition = USE_DEFAULT_TRANSITION; + Object mExitTransition = null; + Object mReenterTransition = USE_DEFAULT_TRANSITION; + Object mSharedElementEnterTransition = null; + Object mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + Boolean mAllowReturnTransitionOverlap; + Boolean mAllowEnterTransitionOverlap; + + SharedElementCallback mEnterTransitionCallback = null; + SharedElementCallback mExitTransitionCallback = null; + + /** + * State information that has been retrieved from a fragment instance + * through {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + */ + public static class SavedState implements Parcelable { + final Bundle mState; + + SavedState(Bundle state) { + mState = state; + } + + SavedState(Parcel in, ClassLoader loader) { + mState = in.readBundle(); + if (loader != null && mState != null) { + mState.setClassLoader(loader); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mState); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Thrown by {@link Fragment#instantiate(Context, String, Bundle)} when + * there is an instantiation failure. + */ + static public class InstantiationException extends RuntimeException { + public InstantiationException(String msg, Exception cause) { + super(msg, cause); + } + } + + /** + * Default constructor. Every fragment must have an + * empty constructor, so it can be instantiated when restoring its + * activity's state. It is strongly recommended that subclasses do not + * have other constructors with parameters, since these constructors + * will not be called when the fragment is re-instantiated; instead, + * arguments can be supplied by the caller with {@link #setArguments} + * and later retrieved by the Fragment with {@link #getArguments}. + * + *

Applications should generally not implement a constructor. The + * first place application code an run where the fragment is ready to + * be used is in {@link #onAttach(Activity)}, the point where the fragment + * is actually associated with its activity. Some applications may also + * want to implement {@link #onInflate} to retrieve attributes from a + * layout resource, though should take care here because this happens for + * the fragment is attached to its activity. + */ + public Fragment() { + } + + /** + * Like {@link #instantiate(Context, String, Bundle)} but with a null + * argument Bundle. + */ + public static Fragment instantiate(Context context, String fname) { + return instantiate(context, fname, null); + } + + /** + * Create a new instance of a Fragment with the given class name. This is + * the same as calling its empty constructor. + * + * @param context The calling context being used to instantiate the fragment. + * This is currently just used to get its ClassLoader. + * @param fname The class name of the fragment to instantiate. + * @param args Bundle of arguments to supply to the fragment, which it + * can retrieve with {@link #getArguments()}. May be null. + * @return Returns a new fragment instance. + * @throws InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) { + try { + Class clazz = sClassMap.get(fname); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = context.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + Fragment f = (Fragment)clazz.newInstance(); + if (args != null) { + args.setClassLoader(f.getClass().getClassLoader()); + f.mArguments = args; + } + return f; + } catch (ClassNotFoundException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (java.lang.InstantiationException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } + } + + /** + * Determine if the given fragment name is a support library fragment class. + * + * @param context Context used to determine the correct ClassLoader to use + * @param fname Class name of the fragment to test + * @return true if fname is android.support.v4.app.Fragment + * or a subclass, false otherwise. + */ + static boolean isSupportFragmentClass(Context context, String fname) { + try { + Class clazz = sClassMap.get(fname); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = context.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + return Fragment.class.isAssignableFrom(clazz); + } catch (ClassNotFoundException e) { + return false; + } + } + + final void restoreViewState(Bundle savedInstanceState) { + if (mSavedViewState != null) { + mInnerView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + mCalled = false; + onViewStateRestored(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onViewStateRestored()"); + } + } + + final void setIndex(int index, Fragment parent) { + mIndex = index; + if (parent != null) { + mWho = parent.mWho + ":" + mIndex; + } else { + mWho = "android:fragment:" + mIndex; + } + } + + final boolean isInBackStack() { + return mBackStackNesting > 0; + } + + /** + * Subclasses can not override equals(). + */ + @Override final public boolean equals(Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override final public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + DebugUtils.buildShortClassTag(this, sb); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + final public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + final public String getTag() { + return mTag; + } + + /** + * Supply the construction arguments for this fragment. This can only + * be called before the fragment has been attached to its activity; that + * is, you should call it immediately after constructing the fragment. The + * arguments supplied here will be retained across fragment destroy and + * creation. + */ + public void setArguments(Bundle args) { + if (mIndex >= 0) { + throw new IllegalStateException("Fragment already active"); + } + mArguments = args; + } + + /** + * Return the arguments supplied when the fragment was instantiated, + * if any. + */ + final public Bundle getArguments() { + return mArguments; + } + + /** + * Set the initial saved state that this Fragment should restore itself + * from when first being constructed, as returned by + * {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + * + * @param state The state the fragment should be restored from. + */ + public void setInitialSavedState(SavedState state) { + if (mIndex >= 0) { + throw new IllegalStateException("Fragment already active"); + } + mSavedFragmentState = state != null && state.mState != null + ? state.mState : null; + } + + /** + * Optional target for this fragment. This may be used, for example, + * if this fragment is being started by another, and when done wants to + * give a result back to the first. The target set here is retained + * across instances via {@link FragmentManager#putFragment + * FragmentManager.putFragment()}. + * + * @param fragment The fragment that is the target of this one. + * @param requestCode Optional request code, for convenience if you + * are going to call back with {@link #onActivityResult(int, int, Intent)}. + */ + public void setTargetFragment(Fragment fragment, int requestCode) { + mTarget = fragment; + mTargetRequestCode = requestCode; + } + + /** + * Return the target fragment set by {@link #setTargetFragment}. + */ + final public Fragment getTargetFragment() { + return mTarget; + } + + /** + * Return the target request code set by {@link #setTargetFragment}. + */ + final public int getTargetRequestCode() { + return mTargetRequestCode; + } + + /** + * Return the Activity this fragment is currently associated with. + */ + final public FragmentActivity getActivity() { + return mActivity; + } + + /** + * Return getActivity().getResources(). + */ + final public Resources getResources() { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + return mActivity.getResources(); + } + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + public final CharSequence getText(@StringRes int resId) { + return getResources().getText(resId); + } + + /** + * Return a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + */ + public final String getString(@StringRes int resId) { + return getResources().getString(resId); + } + + /** + * Return a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + + public final String getString(@StringRes int resId, Object... formatArgs) { + return getResources().getString(resId, formatArgs); + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this fragment's activity. Note that this will be non-null slightly + * before {@link #getActivity()}, during the time from when the fragment is + * placed in a {@link FragmentTransaction} until it is committed and + * attached to its activity. + * + *

If this Fragment is a child of another Fragment, the FragmentManager + * returned here will be the parent's {@link #getChildFragmentManager()}. + */ + final public FragmentManager getFragmentManager() { + return mFragmentManager; + } + + /** + * Return a private FragmentManager for placing and managing Fragments + * inside of this Fragment. + */ + final public FragmentManager getChildFragmentManager() { + if (mChildFragmentManager == null) { + instantiateChildFragmentManager(); + if (mState >= RESUMED) { + mChildFragmentManager.dispatchResume(); + } else if (mState >= STARTED) { + mChildFragmentManager.dispatchStart(); + } else if (mState >= ACTIVITY_CREATED) { + mChildFragmentManager.dispatchActivityCreated(); + } else if (mState >= CREATED) { + mChildFragmentManager.dispatchCreate(); + } + } + return mChildFragmentManager; + } + + /** + * Returns the parent Fragment containing this Fragment. If this Fragment + * is attached directly to an Activity, returns null. + */ + final public Fragment getParentFragment() { + return mParentFragment; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + final public boolean isAdded() { + return mActivity != null && mAdded; + } + + /** + * Return true if the fragment has been explicitly detached from the UI. + * That is, {@link FragmentTransaction#detach(Fragment) + * FragmentTransaction.detach(Fragment)} has been used on it. + */ + final public boolean isDetached() { + return mDetached; + } + + /** + * Return true if this fragment is currently being removed from its + * activity. This is not whether its activity is finishing, but + * rather whether it is in the process of being removed from its activity. + */ + final public boolean isRemoving() { + return mRemoving; + } + + /** + * Return true if the layout is included as part of an activity view + * hierarchy via the <fragment> tag. This will always be true when + * fragments are created through the <fragment> tag, except + * in the case where an old fragment is restored from a previous state and + * it does not appear in the layout of the current state. + */ + final public boolean isInLayout() { + return mInLayout; + } + + /** + * Return true if the fragment is in the resumed state. This is true + * for the duration of {@link #onResume()} and {@link #onPause()} as well. + */ + final public boolean isResumed() { + return mResumed; + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + final public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + final public boolean isHidden() { + return mHidden; + } + + /** @hide */ + final public boolean hasOptionsMenu() { + return mHasMenu; + } + + /** @hide */ + final public boolean isMenuVisible() { + return mMenuVisible; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false if it is not + * visible. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). This can only + * be used with fragments not in the back stack. If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + *

    + *
  • {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + *
  • {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + *
  • {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} will + * still be called. + *
+ */ + public void setRetainInstance(boolean retain) { + if (retain && mParentFragment != null) { + throw new IllegalStateException( + "Can't retain fragements that are nested in other fragments"); + } + mRetainInstance = retain; + } + + final public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Report that this fragment would like to participate in populating + * the options menu by receiving a call to {@link #onCreateOptionsMenu} + * and related methods. + * + * @param hasMenu If true, the fragment has menu items to contribute. + */ + public void setHasOptionsMenu(boolean hasMenu) { + if (mHasMenu != hasMenu) { + mHasMenu = hasMenu; + if (isAdded() && !isHidden()) { + mActivity.supportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint for whether this fragment's menu should be visible. This + * is useful if you know that a fragment has been placed in your view + * hierarchy so that the user can not currently seen it, so any menu items + * it has should also not be shown. + * + * @param menuVisible The default is true, meaning the fragment's menu will + * be shown as usual. If false, the user will not see the menu. + */ + public void setMenuVisibility(boolean menuVisible) { + if (mMenuVisible != menuVisible) { + mMenuVisible = menuVisible; + if (mHasMenu && isAdded() && !isHidden()) { + mActivity.supportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint to the system about whether this fragment's UI is currently visible + * to the user. This hint defaults to true and is persistent across fragment instance + * state save and restore. + * + *

An app may set this to false to indicate that the fragment's UI is + * scrolled out of visibility or is otherwise not directly visible to the user. + * This may be used by the system to prioritize operations such as fragment lifecycle updates + * or loader ordering behavior.

+ * + * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default), + * false if it is not. + */ + public void setUserVisibleHint(boolean isVisibleToUser) { + if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) { + mFragmentManager.performPendingDeferredStart(this); + } + mUserVisibleHint = isVisibleToUser; + mDeferStart = !isVisibleToUser; + } + + /** + * @return The current value of the user-visible hint on this fragment. + * @see #setUserVisibleHint(boolean) + */ + public boolean getUserVisibleHint() { + return mUserVisibleHint; + } + + /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, true); + return mLoaderManager; + } + + /** + * Call {@link Activity#startActivity(Intent)} on the fragment's + * containing Activity. + */ + public void startActivity(Intent intent) { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mActivity.startActivityFromFragment(this, intent, -1); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's + * containing Activity. + */ + public void startActivityForResult(Intent intent, int requestCode) { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mActivity.startActivityFromFragment(this, intent, requestCode); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * @hide Hack so that DialogFragment can make its Dialog before creating + * its views, and the view construction can use the dialog's context for + * inflation. Maybe this should become a public API. Note sure. + */ + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + LayoutInflater result = mActivity.getLayoutInflater().cloneInContext(mActivity); + getChildFragmentManager(); // Init if needed; use raw implementation below. + LayoutInflaterCompat.setFactory(result, mChildFragmentManager.getLayoutInflaterFactory()); + return result; + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * may be called immediately after the fragment is created from a + * tag in a layout file. Note this is before the fragment's + * {@link #onAttach(Activity)} has been called; all you should do here is + * parse the attributes and save them away. + * + *

This is called every time the fragment is inflated, even if it is + * being inflated into a new instance with saved state. It typically makes + * sense to re-parse the parameters each time, to allow them to change with + * different configurations.

+ * + *

Here is a typical implementation of a fragment that can take parameters + * both through attributes supplied here as well from {@link #getArguments()}:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java + * fragment} + * + *

Note that parsing the XML attributes uses a "styleable" resource. The + * declaration for the styleable used here is:

+ * + * {@sample development/samples/ApiDemos/res/values/attrs.xml fragment_arguments} + * + *

The fragment can then be declared within its activity's content layout + * through a tag like this:

+ * + * {@sample development/samples/ApiDemos/res/layout/fragment_arguments.xml from_attributes} + * + *

This fragment can also be created dynamically from arguments given + * at runtime in the arguments Bundle; here is an example of doing so at + * creation of the containing activity:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java + * create} + * + * @param activity The Activity that is inflating this fragment. + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + */ + public void onAttach(Activity activity) { + mCalled = true; + } + + /** + * Called when a fragment loads an animation. + */ + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + *

Note that this can be called while the fragment's activity is + * still in the process of being created. As such, you can not rely + * on things like the activity's content view hierarchy being initialized + * at this point. If you want to do work once the activity itself is + * created, see {@link #onActivityCreated(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onCreate(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. + * + *

If you return a View from here, you will later be called in + * {@link #onDestroyView} when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + @Nullable + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return null; + } + + /** + * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} + * has returned, but before any saved state has been restored in to the view. + * This gives subclasses a chance to initialize themselves once + * they know their view hierarchy has been completely created. The fragment's + * view hierarchy is not however attached to its parent at this point. + * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + */ + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + } + + /** + * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}), + * if provided. + * + * @return The fragment's root view, or null if it has no layout. + */ + @Nullable + public View getView() { + return mView; + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onViewStateRestored(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when all saved state has been restored into the view hierarchy + * of the fragment. This can be used to do initialization based on saved + * state that you are letting the view hierarchy track itself, such as + * whether check box widgets are currently checked. This is called + * after {@link #onActivityCreated(Bundle)} and before + * {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + public void onStart() { + mCalled = true; + + if (!mLoadersStarted) { + mLoadersStarted = true; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } + } + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + public void onResume() { + mCalled = true; + } + + /** + * Called to ask the fragment to save its current dynamic state, so it + * can later be reconstructed in a new instance of its process is + * restarted. If a new instance of the fragment later needs to be + * created, the data you place in the Bundle here will be available + * in the Bundle given to {@link #onCreate(Bundle)}, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and + * {@link #onActivityCreated(Bundle)}. + * + *

This corresponds to {@link Activity#onSaveInstanceState(Bundle) + * Activity.onSaveInstanceState(Bundle)} and most of the discussion there + * applies here as well. Note however: this method may be called + * at any time before {@link #onDestroy()}. There are many situations + * where a fragment may be mostly torn down (such as when placed on the + * back stack with no UI showing), but its state will not be saved until + * its owning activity actually needs to save its state. + * + * @param outState Bundle in which to place your saved state. + */ + public void onSaveInstanceState(Bundle outState) { + } + + public void onConfigurationChanged(Configuration newConfig) { + mCalled = true; + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + public void onStop() { + mCalled = true; + } + + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the view previously created by {@link #onCreateView} has + * been detached from the fragment. The next time the fragment needs + * to be displayed, a new view will be created. This is called + * after {@link #onStop()} and before {@link #onDestroy()}. It is called + * regardless of whether {@link #onCreateView} returned a + * non-null view. Internally it is called after the view's state has + * been saved but before it has been removed from its parent. + */ + public void onDestroyView() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + public void onDestroy() { + mCalled = true; + //Log.v("foo", "onDestroy: mCheckedForLoaderManager=" + mCheckedForLoaderManager + // + " mLoaderManager=" + mLoaderManager); + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Called by the fragment manager once this fragment has been removed, + * so that we don't have any left-over state if the application decides + * to re-use the instance. This only clears state that the framework + * internally manages, not things the application sets. + */ + void initState() { + mIndex = -1; + mWho = null; + mAdded = false; + mRemoving = false; + mResumed = false; + mFromLayout = false; + mInLayout = false; + mRestored = false; + mBackStackNesting = 0; + mFragmentManager = null; + mChildFragmentManager = null; + mActivity = null; + mFragmentId = 0; + mContainerId = 0; + mTag = null; + mHidden = false; + mDetached = false; + mRetaining = false; + mLoaderManager = null; + mLoadersStarted = false; + mCheckedForLoaderManager = false; + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + public void onDetach() { + mCalled = true; + } + + /** + * Initialize the contents of the Activity's standard options menu. You + * should place your menu items in to menu. For this method + * to be called, you must have first called {@link #setHasOptionsMenu}. See + * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu} + * for more information. + * + * @param menu The options menu in which you place your items. + * + * @see #setHasOptionsMenu + * @see #onPrepareOptionsMenu + * @see #onOptionsItemSelected + */ + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + } + + /** + * Prepare the Screen's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. See + * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu} + * for more information. + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + * + * @see #setHasOptionsMenu + * @see #onCreateOptionsMenu + */ + public void onPrepareOptionsMenu(Menu menu) { + } + + /** + * Called when this fragment's option menu items are no longer being + * included in the overall options menu. Receiving this call means that + * the menu needed to be rebuilt, but this fragment's items were not + * included in the newly built menu (its {@link #onCreateOptionsMenu(Menu, MenuInflater)} + * was not called). + */ + public void onDestroyOptionsMenu() { + } + + /** + * This hook is called whenever an item in your options menu is selected. + * The default implementation simply returns false to have the normal + * processing happen (calling the item's Runnable or sending a message to + * its Handler as appropriate). You can use this method for any items + * for which you would like to do processing without those other + * facilities. + * + *

Derived classes should call through to the base class for it to + * perform the default menu handling. + * + * @param item The menu item that was selected. + * + * @return boolean Return false to allow normal menu processing to + * proceed, true to consume it here. + * + * @see #onCreateOptionsMenu + */ + public boolean onOptionsItemSelected(MenuItem item) { + return false; + } + + /** + * This hook is called whenever the options menu is being closed (either by the user canceling + * the menu with the back/menu button, or when an item is selected). + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + */ + public void onOptionsMenuClosed(Menu menu) { + } + + /** + * Called when a context menu for the {@code view} is about to be shown. + * Unlike {@link #onCreateOptionsMenu}, this will be called every + * time the context menu is about to be shown and should be populated for + * the view (or item inside the view for {@link AdapterView} subclasses, + * this can be found in the {@code menuInfo})). + *

+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an + * item has been selected. + *

+ * The default implementation calls up to + * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though + * you can not call this implementation if you don't want that behavior. + *

+ * It is not safe to hold onto the context menu after this method returns. + * {@inheritDoc} + */ + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + getActivity().onCreateContextMenu(menu, v, menuInfo); + } + + /** + * Registers a context menu to be shown for the given view (multiple views + * can show the context menu). This method will set the + * {@link OnCreateContextMenuListener} on the view to this fragment, so + * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be + * called when it is time to show the context menu. + * + * @see #unregisterForContextMenu(View) + * @param view The view that should show a context menu. + */ + public void registerForContextMenu(View view) { + view.setOnCreateContextMenuListener(this); + } + + /** + * Prevents a context menu to be shown for the given view. This method will + * remove the {@link OnCreateContextMenuListener} on the view. + * + * @see #registerForContextMenu(View) + * @param view The view that should stop showing a context menu. + */ + public void unregisterForContextMenu(View view) { + view.setOnCreateContextMenuListener(null); + } + + /** + * This hook is called whenever an item in a context menu is selected. The + * default implementation simply returns false to have the normal processing + * happen (calling the item's Runnable or sending a message to its Handler + * as appropriate). You can use this method for any items for which you + * would like to do processing without those other facilities. + *

+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the + * View that added this menu item. + *

+ * Derived classes should call through to the base class for it to perform + * the default menu handling. + * + * @param item The context menu item that was selected. + * @return boolean Return false to allow normal context menu processing to + * proceed, true to consume it here. + */ + public boolean onContextItemSelected(MenuItem item) { + return false; + } + + /** + * When custom transitions are used with Fragments, the enter transition callback + * is called when this Fragment is attached or detached when not popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added not as a pop from the back stack. + */ + public void setEnterSharedElementCallback(SharedElementCallback callback) { + mEnterTransitionCallback = callback; + } + + /** + * When custom transitions are used with Fragments, the exit transition callback + * is called when this Fragment is attached or detached when popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added as a pop from the back stack. + */ + public void setExitSharedElementCallback(SharedElementCallback callback) { + mExitTransitionCallback = callback; + } + + /** + * Sets the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. If transition is null, + * entering Views will remain unaffected. + * + * @param transition The Transition to use to move Views into the initial Scene. + */ + public void setEnterTransition(Object transition) { + mEnterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. + * + * @return the Transition to use to move Views into the initial Scene. + */ + public Object getEnterTransition() { + return mEnterTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * entering Views will remain unaffected. If nothing is set, the default will be to + * use the same value as set in {@link #setEnterTransition(Object)}. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is preparing to close. transition must be an + * android.transition.Transition. + */ + public void setReturnTransition(Object transition) { + mReturnTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * entering Views will remain unaffected. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is preparing to close. + */ + public Object getReturnTransition() { + return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mReturnTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. transition + * must be an android.transition.Transition. + */ + public void setExitTransition(Object transition) { + mExitTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. + */ + public Object getExitTransition() { + return mExitTransition; + } + + /** + * Sets the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * the views will remain unaffected. If nothing is set, the default will be to use the same + * transition as {@link #setExitTransition(Object)}. + * + * @param transition The Transition to use to move Views into the scene when reentering from a + * previously-started Activity. transition + * must be an android.transition.Transition. + */ + public void setReenterTransition(Object transition) { + mReenterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * the views will remain unaffected. If nothing is set, the default will be to use the same + * transition as {@link #setExitTransition(Object)}. + * + * @return the Transition to use to move Views into the scene when reentering from a + * previously-started Activity. + */ + public Object getReenterTransition() { + return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mReenterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @param transition The Transition to use for shared elements transferred into the content + * Scene. transition must be an android.transition.Transition. + */ + public void setSharedElementEnterTransition(Object transition) { + mSharedElementEnterTransition = transition; + } + + /** + * Returns the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @return The Transition to use for shared elements transferred into the content + * Scene. + */ + public Object getSharedElementEnterTransition() { + return mSharedElementEnterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @param transition The Transition to use for shared elements transferred out of the content + * Scene. transition must be an android.transition.Transition. + */ + public void setSharedElementReturnTransition(Object transition) { + mSharedElementReturnTransition = transition; + } + + /** + * Return the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @return The Transition to use for shared elements transferred out of the content + * Scene. + */ + public Object getSharedElementReturnTransition() { + return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION ? + getSharedElementEnterTransition() : mSharedElementReturnTransition; + } + + /** + * Sets whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @param allow true to start the enter transition when possible or false to + * wait until the exiting transition completes. + */ + public void setAllowEnterTransitionOverlap(boolean allow) { + mAllowEnterTransitionOverlap = allow; + } + + /** + * Returns whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @return true when the enter transition should start as soon as possible or false to + * when it should wait until the exiting transition completes. + */ + public boolean getAllowEnterTransitionOverlap() { + return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap; + } + + /** + * Sets whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @param allow true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public void setAllowReturnTransitionOverlap(boolean allow) { + mAllowReturnTransitionOverlap = allow; + } + + /** + * Returns whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @return true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public boolean getAllowReturnTransitionOverlap() { + return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap; + } + + /** + * Print the Fragments's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mFragmentId=#"); + writer.print(Integer.toHexString(mFragmentId)); + writer.print(" mContainerId=#"); + writer.print(Integer.toHexString(mContainerId)); + writer.print(" mTag="); writer.println(mTag); + writer.print(prefix); writer.print("mState="); writer.print(mState); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mWho="); writer.print(mWho); + writer.print(" mBackStackNesting="); writer.println(mBackStackNesting); + writer.print(prefix); writer.print("mAdded="); writer.print(mAdded); + writer.print(" mRemoving="); writer.print(mRemoving); + writer.print(" mResumed="); writer.print(mResumed); + writer.print(" mFromLayout="); writer.print(mFromLayout); + writer.print(" mInLayout="); writer.println(mInLayout); + writer.print(prefix); writer.print("mHidden="); writer.print(mHidden); + writer.print(" mDetached="); writer.print(mDetached); + writer.print(" mMenuVisible="); writer.print(mMenuVisible); + writer.print(" mHasMenu="); writer.println(mHasMenu); + writer.print(prefix); writer.print("mRetainInstance="); writer.print(mRetainInstance); + writer.print(" mRetaining="); writer.print(mRetaining); + writer.print(" mUserVisibleHint="); writer.println(mUserVisibleHint); + if (mFragmentManager != null) { + writer.print(prefix); writer.print("mFragmentManager="); + writer.println(mFragmentManager); + } + if (mActivity != null) { + writer.print(prefix); writer.print("mActivity="); + writer.println(mActivity); + } + if (mParentFragment != null) { + writer.print(prefix); writer.print("mParentFragment="); + writer.println(mParentFragment); + } + if (mArguments != null) { + writer.print(prefix); writer.print("mArguments="); writer.println(mArguments); + } + if (mSavedFragmentState != null) { + writer.print(prefix); writer.print("mSavedFragmentState="); + writer.println(mSavedFragmentState); + } + if (mSavedViewState != null) { + writer.print(prefix); writer.print("mSavedViewState="); + writer.println(mSavedViewState); + } + if (mTarget != null) { + writer.print(prefix); writer.print("mTarget="); writer.print(mTarget); + writer.print(" mTargetRequestCode="); + writer.println(mTargetRequestCode); + } + if (mNextAnim != 0) { + writer.print(prefix); writer.print("mNextAnim="); writer.println(mNextAnim); + } + if (mContainer != null) { + writer.print(prefix); writer.print("mContainer="); writer.println(mContainer); + } + if (mView != null) { + writer.print(prefix); writer.print("mView="); writer.println(mView); + } + if (mInnerView != null) { + writer.print(prefix); writer.print("mInnerView="); writer.println(mView); + } + if (mAnimatingAway != null) { + writer.print(prefix); writer.print("mAnimatingAway="); writer.println(mAnimatingAway); + writer.print(prefix); writer.print("mStateAfterAnimating="); + writer.println(mStateAfterAnimating); + } + if (mLoaderManager != null) { + writer.print(prefix); writer.println("Loader Manager:"); + mLoaderManager.dump(prefix + " ", fd, writer, args); + } + if (mChildFragmentManager != null) { + writer.print(prefix); writer.println("Child " + mChildFragmentManager + ":"); + mChildFragmentManager.dump(prefix + " ", fd, writer, args); + } + } + + Fragment findFragmentByWho(String who) { + if (who.equals(mWho)) { + return this; + } + if (mChildFragmentManager != null) { + return mChildFragmentManager.findFragmentByWho(who); + } + return null; + } + + void instantiateChildFragmentManager() { + mChildFragmentManager = new FragmentManagerImpl(); + mChildFragmentManager.attachActivity(mActivity, new FragmentContainer() { + @Override + @Nullable + public View findViewById(int id) { + if (mView == null) { + throw new IllegalStateException("Fragment does not have a view"); + } + return mView.findViewById(id); + } + + @Override + public boolean hasView() { + return (mView != null); + } + }, this); + } + + void performCreate(Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + mCalled = false; + onCreate(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onCreate()"); + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable( + FragmentActivity.FRAGMENTS_TAG); + if (p != null) { + if (mChildFragmentManager == null) { + instantiateChildFragmentManager(); + } + mChildFragmentManager.restoreAllState(p, null); + mChildFragmentManager.dispatchCreate(); + } + } + } + + View performCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + return onCreateView(inflater, container, savedInstanceState); + } + + void performActivityCreated(Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + mCalled = false; + onActivityCreated(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onActivityCreated()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchActivityCreated(); + } + } + + void performStart() { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + } + mCalled = false; + onStart(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStart()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchStart(); + } + if (mLoaderManager != null) { + mLoaderManager.doReportStart(); + } + } + + void performResume() { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + } + mCalled = false; + onResume(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onResume()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchResume(); + mChildFragmentManager.execPendingActions(); + } + } + + void performConfigurationChanged(Configuration newConfig) { + onConfigurationChanged(newConfig); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchConfigurationChanged(newConfig); + } + } + + void performLowMemory() { + onLowMemory(); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchLowMemory(); + } + } + + /* + void performTrimMemory(int level) { + onTrimMemory(level); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchTrimMemory(level); + } + } + */ + + boolean performCreateOptionsMenu(Menu menu, MenuInflater inflater) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onCreateOptionsMenu(menu, inflater); + } + if (mChildFragmentManager != null) { + show |= mChildFragmentManager.dispatchCreateOptionsMenu(menu, inflater); + } + } + return show; + } + + boolean performPrepareOptionsMenu(Menu menu) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onPrepareOptionsMenu(menu); + } + if (mChildFragmentManager != null) { + show |= mChildFragmentManager.dispatchPrepareOptionsMenu(menu); + } + } + return show; + } + + boolean performOptionsItemSelected(MenuItem item) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + if (onOptionsItemSelected(item)) { + return true; + } + } + if (mChildFragmentManager != null) { + if (mChildFragmentManager.dispatchOptionsItemSelected(item)) { + return true; + } + } + } + return false; + } + + boolean performContextItemSelected(MenuItem item) { + if (!mHidden) { + if (onContextItemSelected(item)) { + return true; + } + if (mChildFragmentManager != null) { + if (mChildFragmentManager.dispatchContextItemSelected(item)) { + return true; + } + } + } + return false; + } + + void performOptionsMenuClosed(Menu menu) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + onOptionsMenuClosed(menu); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchOptionsMenuClosed(menu); + } + } + } + + void performSaveInstanceState(Bundle outState) { + onSaveInstanceState(outState); + if (mChildFragmentManager != null) { + Parcelable p = mChildFragmentManager.saveAllState(); + if (p != null) { + outState.putParcelable(FragmentActivity.FRAGMENTS_TAG, p); + } + } + } + + void performPause() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchPause(); + } + mCalled = false; + onPause(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onPause()"); + } + } + + void performStop() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchStop(); + } + mCalled = false; + onStop(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStop()"); + } + } + + void performReallyStop() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchReallyStop(); + } + if (mLoadersStarted) { + mLoadersStarted = false; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + if (!mActivity.mRetaining) { + mLoaderManager.doStop(); + } else { + mLoaderManager.doRetain(); + } + } + } + } + + void performDestroyView() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchDestroyView(); + } + mCalled = false; + onDestroyView(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroyView()"); + } + if (mLoaderManager != null) { + mLoaderManager.doReportNextStart(); + } + } + + void performDestroy() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchDestroy(); + } + mCalled = false; + onDestroy(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroy()"); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java new file mode 100644 index 00000000000..566ffed6734 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java @@ -0,0 +1,880 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.SimpleArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Base class for activities that want to use the support-based + * {@link android.support.v4.app.Fragment} and + * {@link android.support.v4.content.Loader} APIs. + * + *

When using this class as opposed to new platform's built-in fragment + * and loader support, you must use the {@link #getSupportFragmentManager()} + * and {@link #getSupportLoaderManager()} methods respectively to access + * those features. + * + *

Note: If you want to implement an activity that includes + * an action bar, you should instead use + * the {@link android.support.v7.app.ActionBarActivity} class, which is a subclass of this one, + * so allows you to use {@link android.support.v4.app.Fragment} APIs on API level 7 and higher.

+ * + *

Known limitations:

+ *
    + *
  • When using the <fragment> tag, this implementation can not + * use the parent view's ID as the new fragment's ID. You must explicitly + * specify an ID (or tag) in the <fragment>.

    + *
  • Prior to Honeycomb (3.0), an activity's state was saved before pausing. + * Fragments are a significant amount of new state, and dynamic enough that one + * often wants them to change between pausing and stopping. These classes + * throw an exception if you try to change the fragment state after it has been + * saved, to avoid accidental loss of UI state. However this is too restrictive + * prior to Honeycomb, where the state is saved before pausing. To address this, + * when running on platforms prior to Honeycomb an exception will not be thrown + * if you change fragments between the state save and the activity being stopped. + * This means that in some cases if the activity is restored from its last saved + * state, this may be a snapshot slightly before what the user last saw.

    + *
+ */ +public class FragmentActivity extends Activity { + private static final String TAG = "FragmentActivity"; + + static final String FRAGMENTS_TAG = "android:support:fragments"; + + // This is the SDK API version of Honeycomb (3.0). + private static final int HONEYCOMB = 11; + + static final int MSG_REALLY_STOPPED = 1; + static final int MSG_RESUME_PENDING = 2; + + final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REALLY_STOPPED: + if (mStopped) { + doReallyStop(false); + } + break; + case MSG_RESUME_PENDING: + onResumeFragments(); + mFragments.execPendingActions(); + break; + default: + super.handleMessage(msg); + } + } + + }; + final FragmentManagerImpl mFragments = new FragmentManagerImpl(); + final FragmentContainer mContainer = new FragmentContainer() { + @Override + @Nullable + public View findViewById(int id) { + return FragmentActivity.this.findViewById(id); + } + + @Override + public boolean hasView() { + Window window = FragmentActivity.this.getWindow(); + return (window != null && window.peekDecorView() != null); + } + }; + + boolean mCreated; + boolean mResumed; + boolean mStopped; + boolean mReallyStopped; + boolean mRetaining; + + boolean mOptionsMenuInvalidated; + + boolean mCheckedForLoaderManager; + boolean mLoadersStarted; + SimpleArrayMap mAllLoaderManagers; + LoaderManagerImpl mLoaderManager; + + static final class NonConfigurationInstances { + Object activity; + Object custom; + SimpleArrayMap children; + ArrayList fragments; + SimpleArrayMap loaders; + } + + // ------------------------------------------------------------------------ + // HOOKS INTO ACTIVITY + // ------------------------------------------------------------------------ + + /** + * Dispatch incoming result to the correct fragment. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mFragments.noteStateNotSaved(); + int index = requestCode>>16; + if (index != 0) { + index--; + if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) { + Log.w(TAG, "Activity result fragment index out of range: 0x" + + Integer.toHexString(requestCode)); + return; + } + Fragment frag = mFragments.mActive.get(index); + if (frag == null) { + Log.w(TAG, "Activity result no fragment exists for index: 0x" + + Integer.toHexString(requestCode)); + } else { + frag.onActivityResult(requestCode&0xffff, resultCode, data); + } + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Take care of popping the fragment back stack or finishing the activity + * as appropriate. + */ + public void onBackPressed() { + if (!mFragments.popBackStackImmediate()) { + supportFinishAfterTransition(); + } + } + + /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link #finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * + *

On Android 4.4 or lower, this method only finishes the Activity with no + * special exit transition.

+ */ + public void supportFinishAfterTransition() { + ActivityCompat.finishAfterTransition(this); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launched Activity. This requires + * {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launched Activity. + */ + public void setEnterSharedElementCallback(SharedElementCallback callback) { + ActivityCompat.setEnterSharedElementCallback(this, callback); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, listener + * will be called to handle shared elements on the launching Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param listener Used to manipulate shared element transitions on the launching Activity. + */ + public void setExitSharedElementCallback(SharedElementCallback listener) { + ActivityCompat.setExitSharedElementCallback(this, listener); + } + + /** + * Support library version of {@link android.app.Activity#postponeEnterTransition()} that works + * only on API 21 and later. + */ + public void supportPostponeEnterTransition() { + ActivityCompat.postponeEnterTransition(this); + } + + /** + * Support library version of {@link android.app.Activity#startPostponedEnterTransition()} + * that only works with API 21 and later. + */ + public void supportStartPostponedEnterTransition() { + ActivityCompat.startPostponedEnterTransition(this); + } + + /** + * Dispatch configuration change to all fragments. + */ + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mFragments.dispatchConfigurationChanged(newConfig); + } + + /** + * Perform initialization of all fragments and loaders. + */ + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + mFragments.attachActivity(this, mContainer, null); + // Old versions of the platform didn't do this! + if (getLayoutInflater().getFactory() == null) { + getLayoutInflater().setFactory(this); + } + + super.onCreate(savedInstanceState); + + NonConfigurationInstances nc = (NonConfigurationInstances) + getLastNonConfigurationInstance(); + if (nc != null) { + mAllLoaderManagers = nc.loaders; + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreAllState(p, nc != null ? nc.fragments : null); + } + mFragments.dispatchCreate(); + } + + /** + * Dispatch to Fragment.onCreateOptionsMenu(). + */ + @Override + public boolean onCreatePanelMenu(int featureId, Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean show = super.onCreatePanelMenu(featureId, menu); + show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + return show; + } + // Prior to Honeycomb, the framework can't invalidate the options + // menu, so we must always say we have one in case the app later + // invalidates it and needs to have it shown. + return true; + } + return super.onCreatePanelMenu(featureId, menu); + } + + /** + * Add support for inflating the <fragment> tag. + */ + @Override + @Nullable + public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) { + if (!"fragment".equals(name)) { + return super.onCreateView(name, context, attrs); + } + + final View v = mFragments.onCreateView(null, name, context, attrs); + if (v == null) { + return super.onCreateView(name, context, attrs); + } + return v; + } + + /** + * Destroy all fragments and loaders. + */ + @Override + protected void onDestroy() { + super.onDestroy(); + + doReallyStop(false); + + mFragments.dispatchDestroy(); + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Take care of calling onBackPressed() for pre-Eclair platforms. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (android.os.Build.VERSION.SDK_INT < 5 /* ECLAIR */ + && keyCode == KeyEvent.KEYCODE_BACK + && event.getRepeatCount() == 0) { + // Take care of calling this method on earlier versions of + // the platform where it doesn't exist. + onBackPressed(); + return true; + } + + return super.onKeyDown(keyCode, event); + } + + /** + * Dispatch onLowMemory() to all fragments. + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + mFragments.dispatchLowMemory(); + } + + /** + * Dispatch context and options menu to fragments. + */ + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + if (super.onMenuItemSelected(featureId, item)) { + return true; + } + + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + return mFragments.dispatchOptionsItemSelected(item); + + case Window.FEATURE_CONTEXT_MENU: + return mFragments.dispatchContextItemSelected(item); + + default: + return false; + } + } + + /** + * Call onOptionsMenuClosed() on fragments. + */ + @Override + public void onPanelClosed(int featureId, Menu menu) { + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + mFragments.dispatchOptionsMenuClosed(menu); + break; + } + super.onPanelClosed(featureId, menu); + } + + /** + * Dispatch onPause() to fragments. + */ + @Override + protected void onPause() { + super.onPause(); + mResumed = false; + if (mHandler.hasMessages(MSG_RESUME_PENDING)) { + mHandler.removeMessages(MSG_RESUME_PENDING); + onResumeFragments(); + } + mFragments.dispatchPause(); + } + + /** + * Handle onNewIntent() to inform the fragment manager that the + * state is not saved. If you are handling new intents and may be + * making changes to the fragment state, you want to be sure to call + * through to the super-class here first. Otherwise, if your state + * is saved but the activity is not stopped, you could get an + * onNewIntent() call which happens before onResume() and trying to + * perform fragment operations at that point will throw IllegalStateException + * because the fragment manager thinks the state is still saved. + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + mFragments.noteStateNotSaved(); + } + + /** + * Dispatch onResume() to fragments. Note that for better inter-operation + * with older versions of the platform, at the point of this call the + * fragments attached to the activity are not resumed. This means + * that in some cases the previous state may still be saved, not allowing + * fragment transactions that modify the state. To correctly interact + * with fragments in their proper state, you should instead override + * {@link #onResumeFragments()}. + */ + @Override + protected void onResume() { + super.onResume(); + mHandler.sendEmptyMessage(MSG_RESUME_PENDING); + mResumed = true; + mFragments.execPendingActions(); + } + + /** + * Dispatch onResume() to fragments. + */ + @Override + protected void onPostResume() { + super.onPostResume(); + mHandler.removeMessages(MSG_RESUME_PENDING); + onResumeFragments(); + mFragments.execPendingActions(); + } + + /** + * This is the fragment-orientated version of {@link #onResume()} that you + * can override to perform operations in the Activity at the same point + * where its fragments are resumed. Be sure to always call through to + * the super-class. + */ + protected void onResumeFragments() { + mFragments.dispatchResume(); + } + + /** + * Dispatch onPrepareOptionsMenu() to fragments. + */ + @Override + public boolean onPreparePanel(int featureId, View view, Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { + if (mOptionsMenuInvalidated) { + mOptionsMenuInvalidated = false; + menu.clear(); + onCreatePanelMenu(featureId, menu); + } + boolean goforit = onPrepareOptionsPanel(view, menu); + goforit |= mFragments.dispatchPrepareOptionsMenu(menu); + return goforit; + } + return super.onPreparePanel(featureId, view, menu); + } + + /** + * @hide + */ + protected boolean onPrepareOptionsPanel(View view, Menu menu) { + return super.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, view, menu); + } + + /** + * Retain all appropriate fragment and loader state. You can NOT + * override this yourself! Use {@link #onRetainCustomNonConfigurationInstance()} + * if you want to retain your own state. + */ + @Override + public final Object onRetainNonConfigurationInstance() { + if (mStopped) { + doReallyStop(true); + } + + Object custom = onRetainCustomNonConfigurationInstance(); + + ArrayList fragments = mFragments.retainNonConfig(); + boolean retainLoaders = false; + if (mAllLoaderManagers != null) { + // prune out any loader managers that were already stopped and so + // have nothing useful to retain. + final int N = mAllLoaderManagers.size(); + LoaderManagerImpl loaders[] = new LoaderManagerImpl[N]; + for (int i=N-1; i>=0; i--) { + loaders[i] = mAllLoaderManagers.valueAt(i); + } + for (int i=0; i=0; i--) { + loaders[i] = mAllLoaderManagers.valueAt(i); + } + for (int i=0; iInvalidate the activity's options menu. This will cause relevant presentations + * of the menu to fully update via calls to onCreateOptionsMenu and + * onPrepareOptionsMenu the next time the menu is requested. + */ + public void supportInvalidateOptionsMenu() { + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + // If we are running on HC or greater, we can use the framework + // API to invalidate the options menu. + ActivityCompatHoneycomb.invalidateOptionsMenu(this); + return; + } + + // Whoops, older platform... we'll use a hack, to manually rebuild + // the options menu the next time it is prepared. + mOptionsMenuInvalidated = true; + } + + /** + * Print the Activity's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity ". + * + * @param prefix Desired prefix to prepend at each line of output. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + // XXX This can only work if we can call the super-class impl. :/ + //ActivityCompatHoneycomb.dump(this, prefix, fd, writer, args); + } + writer.print(prefix); writer.print("Local FragmentActivity "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(" State:"); + String innerPrefix = prefix + " "; + writer.print(innerPrefix); writer.print("mCreated="); + writer.print(mCreated); writer.print("mResumed="); + writer.print(mResumed); writer.print(" mStopped="); + writer.print(mStopped); writer.print(" mReallyStopped="); + writer.println(mReallyStopped); + writer.print(innerPrefix); writer.print("mLoadersStarted="); + writer.println(mLoadersStarted); + if (mLoaderManager != null) { + writer.print(prefix); writer.print("Loader Manager "); + writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager))); + writer.println(":"); + mLoaderManager.dump(prefix + " ", fd, writer, args); + } + mFragments.dump(prefix, fd, writer, args); + writer.print(prefix); writer.println("View Hierarchy:"); + dumpViewHierarchy(prefix + " ", writer, getWindow().getDecorView()); + } + + private static String viewToString(View view) { + StringBuilder out = new StringBuilder(128); + out.append(view.getClass().getName()); + out.append('{'); + out.append(Integer.toHexString(System.identityHashCode(view))); + out.append(' '); + switch (view.getVisibility()) { + case View.VISIBLE: out.append('V'); break; + case View.INVISIBLE: out.append('I'); break; + case View.GONE: out.append('G'); break; + default: out.append('.'); break; + } + out.append(view.isFocusable() ? 'F' : '.'); + out.append(view.isEnabled() ? 'E' : '.'); + out.append(view.willNotDraw() ? '.' : 'D'); + out.append(view.isHorizontalScrollBarEnabled()? 'H' : '.'); + out.append(view.isVerticalScrollBarEnabled() ? 'V' : '.'); + out.append(view.isClickable() ? 'C' : '.'); + out.append(view.isLongClickable() ? 'L' : '.'); + out.append(' '); + out.append(view.isFocused() ? 'F' : '.'); + out.append(view.isSelected() ? 'S' : '.'); + out.append(view.isPressed() ? 'P' : '.'); + out.append(' '); + out.append(view.getLeft()); + out.append(','); + out.append(view.getTop()); + out.append('-'); + out.append(view.getRight()); + out.append(','); + out.append(view.getBottom()); + final int id = view.getId(); + if (id != View.NO_ID) { + out.append(" #"); + out.append(Integer.toHexString(id)); + final Resources r = view.getResources(); + if (id != 0 && r != null) { + try { + String pkgname; + switch (id&0xff000000) { + case 0x7f000000: + pkgname="app"; + break; + case 0x01000000: + pkgname="android"; + break; + default: + pkgname = r.getResourcePackageName(id); + break; + } + String typename = r.getResourceTypeName(id); + String entryname = r.getResourceEntryName(id); + out.append(" "); + out.append(pkgname); + out.append(":"); + out.append(typename); + out.append("/"); + out.append(entryname); + } catch (Resources.NotFoundException e) { + } + } + } + out.append("}"); + return out.toString(); + } + + private void dumpViewHierarchy(String prefix, PrintWriter writer, View view) { + writer.print(prefix); + if (view == null) { + writer.println("null"); + return; + } + writer.println(viewToString(view)); + if (!(view instanceof ViewGroup)) { + return; + } + ViewGroup grp = (ViewGroup)view; + final int N = grp.getChildCount(); + if (N <= 0) { + return; + } + prefix = prefix + " "; + for (int i=0; i(); + } + LoaderManagerImpl lm = mAllLoaderManagers.get(who); + if (lm == null) { + if (create) { + lm = new LoaderManagerImpl(who, this, started); + mAllLoaderManagers.put(who, lm); + } + } else { + lm.updateActivity(this); + } + return lm; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java new file mode 100644 index 00000000000..6d41d455692 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java @@ -0,0 +1,2224 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IdRes; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.util.DebugUtils; +import android.support.v4.util.LogWriter; +import android.support.v4.view.LayoutInflaterFactory; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.Animation.AnimationListener; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Static library support version of the framework's {@link android.app.FragmentManager}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link FragmentManager} + * documentation for a class overview. + * + *

Your activity must derive from {@link FragmentActivity} to use this. From such an activity, + * you can acquire the {@link FragmentManager} by calling + * {@link FragmentActivity#getSupportFragmentManager}. + */ +public abstract class FragmentManager { + /** + * Representation of an entry on the fragment back stack, as created + * with {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack()}. Entries can later be + * retrieved with {@link FragmentManager#getBackStackEntryAt(int) + * FragmentManager.getBackStackEntry()}. + * + *

Note that you should never hold on to a BackStackEntry object; + * the identifier as returned by {@link #getId} is the only thing that + * will be persisted across activity instances. + */ + public interface BackStackEntry { + /** + * Return the unique identifier for the entry. This is the only + * representation of the entry that will persist across activity + * instances. + */ + public int getId(); + + /** + * Get the name that was supplied to + * {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack(String)} when creating this entry. + */ + public String getName(); + + /** + * Return the full bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbTitleRes(); + + /** + * Return the short bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbShortTitleRes(); + + /** + * Return the full bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbTitle(); + + /** + * Return the short bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbShortTitle(); + } + + /** + * Interface to watch for changes to the back stack. + */ + public interface OnBackStackChangedListener { + /** + * Called whenever the contents of the back stack change. + */ + public void onBackStackChanged(); + } + + /** + * Start a series of edit operations on the Fragments associated with + * this FragmentManager. + * + *

Note: A fragment transaction can only be created/committed prior + * to an activity saving its state. If you try to commit a transaction + * after {@link FragmentActivity#onSaveInstanceState FragmentActivity.onSaveInstanceState()} + * (and prior to a following {@link FragmentActivity#onStart FragmentActivity.onStart} + * or {@link FragmentActivity#onResume FragmentActivity.onResume()}, you will get an error. + * This is because the framework takes care of saving your current fragments + * in the state, and if changes are made after the state is saved then they + * will be lost.

+ */ + public abstract FragmentTransaction beginTransaction(); + + /** @hide -- remove once prebuilts are in. */ + @Deprecated + public FragmentTransaction openTransaction() { + return beginTransaction(); + } + + /** + * After a {@link FragmentTransaction} is committed with + * {@link FragmentTransaction#commit FragmentTransaction.commit()}, it + * is scheduled to be executed asynchronously on the process's main thread. + * If you want to immediately executing any such pending operations, you + * can call this function (only from the main thread) to do so. Note that + * all callbacks and other related behavior will be done from within this + * call, so be careful about where this is called from. + * + * @return Returns true if there were any pending transactions to be + * executed. + */ + public abstract boolean executePendingTransactions(); + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack associated with this ID are searched. + * @return The fragment if found or null otherwise. + */ + public abstract Fragment findFragmentById(@IdRes int id); + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack are searched. + * @return The fragment if found or null otherwise. + */ + public abstract Fragment findFragmentByTag(String tag); + + /** + * Flag for {@link #popBackStack(String, int)} + * and {@link #popBackStack(int, int)}: If set, and the name or ID of + * a back stack entry has been supplied, then all matching entries will + * be consumed until one that doesn't match is found or the bottom of + * the stack is reached. Otherwise, all entries up to but not including that entry + * will be removed. + */ + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; + + /** + * Pop the top state off the back stack. Returns true if there was one + * to pop, else false. This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + */ + public abstract void popBackStack(); + + /** + * Like {@link #popBackStack()}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(); + + /** + * Pop the last fragment transition from the manager's fragment + * back stack. If there is nothing to pop, false is returned. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to that state will be popped. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. If null, only the top state is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(String name, int flags); + + /** + * Like {@link #popBackStack(String, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(String name, int flags); + + /** + * Pop all back stack states up to the one with the given identifier. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param id Identifier of the stated to be popped. If no identifier exists, + * false is returned. + * The identifier is the number returned by + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(int id, int flags); + + /** + * Like {@link #popBackStack(int, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(int id, int flags); + + /** + * Return the number of entries currently in the back stack. + */ + public abstract int getBackStackEntryCount(); + + /** + * Return the BackStackEntry at index index in the back stack; + * entries start index 0 being the bottom of the stack. + */ + public abstract BackStackEntry getBackStackEntryAt(int index); + + /** + * Add a new listener for changes to the fragment back stack. + */ + public abstract void addOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Remove a listener that was previously added with + * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}. + */ + public abstract void removeOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Put a reference to a fragment in a Bundle. This Bundle can be + * persisted as saved state, and when later restoring + * {@link #getFragment(Bundle, String)} will return the current + * instance of the same fragment. + * + * @param bundle The bundle in which to put the fragment reference. + * @param key The name of the entry in the bundle. + * @param fragment The Fragment whose reference is to be stored. + */ + public abstract void putFragment(Bundle bundle, String key, Fragment fragment); + + /** + * Retrieve the current Fragment instance for a reference previously + * placed with {@link #putFragment(Bundle, String, Fragment)}. + * + * @param bundle The bundle from which to retrieve the fragment reference. + * @param key The name of the entry in the bundle. + * @return Returns the current Fragment instance that is associated with + * the given reference. + */ + public abstract Fragment getFragment(Bundle bundle, String key); + + /** + * Get a list of all fragments that have been added to the fragment manager. + * + * @return The list of all fragments or null if none. + * @hide + */ + public abstract List getFragments(); + + /** + * Save the current instance state of the given Fragment. This can be + * used later when creating a new instance of the Fragment and adding + * it to the fragment manager, to have it create itself to match the + * current state returned here. Note that there are limits on how + * this can be used: + * + *
    + *
  • The Fragment must currently be attached to the FragmentManager. + *
  • A new Fragment created using this saved state must be the same class + * type as the Fragment it was created from. + *
  • The saved state can not contain dependencies on other fragments -- + * that is it can't use {@link #putFragment(Bundle, String, Fragment)} to + * store a fragment reference because that reference may not be valid when + * this saved state is later used. Likewise the Fragment's target and + * result code are not included in this state. + *
+ * + * @param f The Fragment whose state is to be saved. + * @return The generated state. This will be null if there was no + * interesting state created by the fragment. + */ + public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f); + + /** + * Returns true if the final {@link android.app.Activity#onDestroy() Activity.onDestroy()} + * call has been made on the FragmentManager's Activity, so this instance is now dead. + */ + public abstract boolean isDestroyed(); + + /** + * Print the FragmentManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args); + + /** + * Control whether the framework's internal fragment manager debugging + * logs are turned on. If enabled, you will see output in logcat as + * the framework performs fragment operations. + */ + public static void enableDebugLogging(boolean enabled) { + FragmentManagerImpl.DEBUG = enabled; + } +} + +final class FragmentManagerState implements Parcelable { + FragmentState[] mActive; + int[] mAdded; + BackStackState[] mBackStack; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArray(FragmentState.CREATOR); + mAdded = in.createIntArray(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedArray(mActive, flags); + dest.writeIntArray(mAdded); + dest.writeTypedArray(mBackStack, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} + +/** + * Callbacks from FragmentManagerImpl to its container. + */ +interface FragmentContainer { + @Nullable + public View findViewById(@IdRes int id); + public boolean hasView(); +} + +/** + * Container for fragments associated with an activity. + */ +final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory { + static boolean DEBUG = false; + static final String TAG = "FragmentManager"; + + static final boolean HONEYCOMB = android.os.Build.VERSION.SDK_INT >= 11; + + static final String TARGET_REQUEST_CODE_STATE_TAG = "android:target_req_state"; + static final String TARGET_STATE_TAG = "android:target_state"; + static final String VIEW_STATE_TAG = "android:view_state"; + static final String USER_VISIBLE_HINT_TAG = "android:user_visible_hint"; + + ArrayList mPendingActions; + Runnable[] mTmpActions; + boolean mExecutingActions; + + ArrayList mActive; + ArrayList mAdded; + ArrayList mAvailIndices; + ArrayList mBackStack; + ArrayList mCreatedMenus; + + // Must be accessed while locked. + ArrayList mBackStackIndices; + ArrayList mAvailBackStackIndices; + + ArrayList mBackStackChangeListeners; + + int mCurState = Fragment.INITIALIZING; + FragmentActivity mActivity; + FragmentContainer mContainer; + Fragment mParent; + + boolean mNeedMenuInvalidate; + boolean mStateSaved; + boolean mDestroyed; + String mNoTransactionsBecause; + boolean mHavePendingDeferredStart; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray mStateArray = null; + + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + + private void throwException(RuntimeException ex) { + Log.e(TAG, ex.getMessage()); + Log.e(TAG, "Activity state:"); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + if (mActivity != null) { + try { + mActivity.dump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } else { + try { + dump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } + throw ex; + } + + @Override + public FragmentTransaction beginTransaction() { + return new BackStackRecord(this); + } + + @Override + public boolean executePendingTransactions() { + return execPendingActions(); + } + + @Override + public void popBackStack() { + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, null, -1, 0); + } + }, false); + } + + @Override + public boolean popBackStackImmediate() { + checkStateLoss(); + executePendingTransactions(); + return popBackStackState(mActivity.mHandler, null, -1, 0); + } + + @Override + public void popBackStack(final String name, final int flags) { + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, name, -1, flags); + } + }, false); + } + + @Override + public boolean popBackStackImmediate(String name, int flags) { + checkStateLoss(); + executePendingTransactions(); + return popBackStackState(mActivity.mHandler, name, -1, flags); + } + + @Override + public void popBackStack(final int id, final int flags) { + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, null, id, flags); + } + }, false); + } + + @Override + public boolean popBackStackImmediate(int id, int flags) { + checkStateLoss(); + executePendingTransactions(); + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + return popBackStackState(mActivity.mHandler, null, id, flags); + } + + @Override + public int getBackStackEntryCount() { + return mBackStack != null ? mBackStack.size() : 0; + } + + @Override + public BackStackEntry getBackStackEntryAt(int index) { + return mBackStack.get(index); + } + + @Override + public void addOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners == null) { + mBackStackChangeListeners = new ArrayList(); + } + mBackStackChangeListeners.add(listener); + } + + @Override + public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners != null) { + mBackStackChangeListeners.remove(listener); + } + } + + @Override + public void putFragment(Bundle bundle, String key, Fragment fragment) { + if (fragment.mIndex < 0) { + throwException(new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + bundle.putInt(key, fragment.mIndex); + } + + @Override + public Fragment getFragment(Bundle bundle, String key) { + int index = bundle.getInt(key, -1); + if (index == -1) { + return null; + } + if (index >= mActive.size()) { + throwException(new IllegalStateException("Fragment no longer exists for key " + + key + ": index " + index)); + } + Fragment f = mActive.get(index); + if (f == null) { + throwException(new IllegalStateException("Fragment no longer exists for key " + + key + ": index " + index)); + } + return f; + } + + @Override + public List getFragments() { + return mActive; + } + + @Override + public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) { + if (fragment.mIndex < 0) { + throwException( new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + if (fragment.mState > Fragment.INITIALIZING) { + Bundle result = saveFragmentBasicState(fragment); + return result != null ? new Fragment.SavedState(result) : null; + } + return null; + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FragmentManager{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" in "); + if (mParent != null) { + DebugUtils.buildShortClassTag(mParent, sb); + } else { + DebugUtils.buildShortClassTag(mActivity, sb); + } + sb.append("}}"); + return sb.toString(); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + String innerPrefix = prefix + " "; + + int N; + if (mActive != null) { + N = mActive.size(); + if (N > 0) { + writer.print(prefix); writer.print("Active Fragments in "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(":"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Added Fragments:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Fragments Created Menus:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack Indices:"); + for (int i=0; i 0) { + writer.print(prefix); writer.print("mAvailBackStackIndices: "); + writer.println(Arrays.toString(mAvailBackStackIndices.toArray())); + } + } + + if (mPendingActions != null) { + N = mPendingActions.size(); + if (N > 0) { + writer.print(prefix); writer.println("Pending Actions:"); + for (int i=0; i Fragment.CREATED) { + newState = Fragment.CREATED; + } + if (f.mRemoving && newState > f.mState) { + // While removing a fragment, we can't change it to a higher state. + newState = f.mState; + } + // Defer start if requested; don't allow it to move to STARTED or higher + // if it's not already started. + if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.STOPPED) { + newState = Fragment.STOPPED; + } + if (f.mState < newState) { + // For fragments that are created from a layout, when restoring from + // state we don't want to allow them to be created until they are + // being reloaded from the layout. + if (f.mFromLayout && !f.mInLayout) { + return; + } + if (f.mAnimatingAway != null) { + // The fragment is currently being animated... but! Now we + // want to move our state back up. Give up on waiting for the + // animation, move to whatever the final state should be once + // the animation is done, and then we can proceed from there. + f.mAnimatingAway = null; + moveToState(f, f.mStateAfterAnimating, 0, 0, true); + } + switch (f.mState) { + case Fragment.INITIALIZING: + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + if (f.mSavedFragmentState != null) { + f.mSavedFragmentState.setClassLoader(mActivity.getClassLoader()); + f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG); + f.mTarget = getFragment(f.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG); + if (f.mTarget != null) { + f.mTargetRequestCode = f.mSavedFragmentState.getInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); + } + f.mUserVisibleHint = f.mSavedFragmentState.getBoolean( + FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true); + if (!f.mUserVisibleHint) { + f.mDeferStart = true; + if (newState > Fragment.STOPPED) { + newState = Fragment.STOPPED; + } + } + } + f.mActivity = mActivity; + f.mParentFragment = mParent; + f.mFragmentManager = mParent != null + ? mParent.mChildFragmentManager : mActivity.mFragments; + f.mCalled = false; + f.onAttach(mActivity); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onAttach()"); + } + if (f.mParentFragment == null) { + mActivity.onAttachFragment(f); + } + + if (!f.mRetaining) { + f.performCreate(f.mSavedFragmentState); + } + f.mRetaining = false; + if (f.mFromLayout) { + // For fragments that are part of the content view + // layout, we need to instantiate the view immediately + // and the inflater will take care of adding it. + f.mView = f.performCreateView(f.getLayoutInflater( + f.mSavedFragmentState), null, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + if (Build.VERSION.SDK_INT >= 11) { + ViewCompat.setSaveFromParentEnabled(f.mView, false); + } else { + f.mView = NoSaveStateFrameLayout.wrap(f.mView); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); + } else { + f.mInnerView = null; + } + } + case Fragment.CREATED: + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + container = (ViewGroup)mContainer.findViewById(f.mContainerId); + if (container == null && !f.mRestored) { + throwException(new IllegalArgumentException( + "No view found for id 0x" + + Integer.toHexString(f.mContainerId) + " (" + + f.getResources().getResourceName(f.mContainerId) + + ") for fragment " + f)); + } + } + f.mContainer = container; + f.mView = f.performCreateView(f.getLayoutInflater( + f.mSavedFragmentState), container, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + if (Build.VERSION.SDK_INT >= 11) { + ViewCompat.setSaveFromParentEnabled(f.mView, false); + } else { + f.mView = NoSaveStateFrameLayout.wrap(f.mView); + } + if (container != null) { + Animation anim = loadAnimation(f, transit, true, + transitionStyle); + if (anim != null) { + f.mView.startAnimation(anim); + } + container.addView(f.mView); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); + } else { + f.mInnerView = null; + } + } + + f.performActivityCreated(f.mSavedFragmentState); + if (f.mView != null) { + f.restoreViewState(f.mSavedFragmentState); + } + f.mSavedFragmentState = null; + } + case Fragment.ACTIVITY_CREATED: + case Fragment.STOPPED: + if (newState > Fragment.STOPPED) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.performStart(); + } + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.mResumed = true; + f.performResume(); + f.mSavedFragmentState = null; + f.mSavedViewState = null; + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.performPause(); + f.mResumed = false; + } + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.performStop(); + } + case Fragment.STOPPED: + if (newState < Fragment.STOPPED) { + if (DEBUG) Log.v(TAG, "movefrom STOPPED: " + f); + f.performReallyStop(); + } + case Fragment.ACTIVITY_CREATED: + if (newState < Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); + if (f.mView != null) { + // Need to save the current view state if not + // done already. + if (!mActivity.isFinishing() && f.mSavedViewState == null) { + saveFragmentViewState(f); + } + } + f.performDestroyView(); + if (f.mView != null && f.mContainer != null) { + Animation anim = null; + if (mCurState > Fragment.INITIALIZING && !mDestroyed) { + anim = loadAnimation(f, transit, false, + transitionStyle); + } + if (anim != null) { + final Fragment fragment = f; + f.mAnimatingAway = f.mView; + f.mStateAfterAnimating = newState; + anim.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + if (fragment.mAnimatingAway != null) { + fragment.mAnimatingAway = null; + moveToState(fragment, fragment.mStateAfterAnimating, + 0, 0, false); + } + } + @Override + public void onAnimationRepeat(Animation animation) { + } + @Override + public void onAnimationStart(Animation animation) { + } + }); + f.mView.startAnimation(anim); + } + f.mContainer.removeView(f.mView); + } + f.mContainer = null; + f.mView = null; + f.mInnerView = null; + } + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (mDestroyed) { + if (f.mAnimatingAway != null) { + // The fragment's containing activity is + // being destroyed, but this fragment is + // currently animating away. Stop the + // animation right now -- it is not needed, + // and we can't wait any more on destroying + // the fragment. + View v = f.mAnimatingAway; + f.mAnimatingAway = null; + v.clearAnimation(); + } + } + if (f.mAnimatingAway != null) { + // We are waiting for the fragment's view to finish + // animating away. Just make a note of the state + // the fragment now should move to once the animation + // is done. + f.mStateAfterAnimating = newState; + newState = Fragment.CREATED; + } else { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + if (!f.mRetaining) { + f.performDestroy(); + } + + f.mCalled = false; + f.onDetach(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDetach()"); + } + if (!keepActive) { + if (!f.mRetaining) { + makeInactive(f); + } else { + f.mActivity = null; + f.mParentFragment = null; + f.mFragmentManager = null; + f.mChildFragmentManager = null; + } + } + } + } + } + } + + f.mState = newState; + } + + void moveToState(Fragment f) { + moveToState(f, mCurState, 0, 0, false); + } + + void moveToState(int newState, boolean always) { + moveToState(newState, 0, 0, always); + } + + void moveToState(int newState, int transit, int transitStyle, boolean always) { + if (mActivity == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && mCurState == newState) { + return; + } + + mCurState = newState; + if (mActive != null) { + boolean loadersRunning = false; + for (int i=0; i= 0) { + return; + } + + if (mAvailIndices == null || mAvailIndices.size() <= 0) { + if (mActive == null) { + mActive = new ArrayList(); + } + f.setIndex(mActive.size(), mParent); + mActive.add(f); + + } else { + f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent); + mActive.set(f.mIndex, f); + } + if (DEBUG) Log.v(TAG, "Allocated fragment index " + f); + } + + void makeInactive(Fragment f) { + if (f.mIndex < 0) { + return; + } + + if (DEBUG) Log.v(TAG, "Freeing fragment index " + f); + mActive.set(f.mIndex, null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList(); + } + mAvailIndices.add(f.mIndex); + mActivity.invalidateSupportFragment(f.mWho); + f.initState(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (mAdded == null) { + mAdded = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "add: " + fragment); + makeActive(fragment); + if (!fragment.mDetached) { + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + mAdded.add(fragment); + fragment.mAdded = true; + fragment.mRemoving = false; + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment); + } + } + } + + public void removeFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting); + final boolean inactive = !fragment.isInBackStack(); + if (!fragment.mDetached || inactive) { + if (mAdded != null) { + mAdded.remove(fragment); + } + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + fragment.mRemoving = true; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle, false); + } + } + + public void hideFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, false, + transitionStyle); + if (anim != null) { + fragment.mView.startAnimation(anim); + } + fragment.mView.setVisibility(View.GONE); + } + if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(true); + } + } + + public void showFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, true, + transitionStyle); + if (anim != null) { + fragment.mView.startAnimation(anim); + } + fragment.mView.setVisibility(View.VISIBLE); + } + if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(false); + } + } + + public void detachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "detach: " + fragment); + if (!fragment.mDetached) { + fragment.mDetached = true; + if (fragment.mAdded) { + // We are not already in back stack, so need to remove the fragment. + if (mAdded != null) { + if (DEBUG) Log.v(TAG, "remove from detach: " + fragment); + mAdded.remove(fragment); + } + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + moveToState(fragment, Fragment.CREATED, transition, transitionStyle, false); + } + } + } + + public void attachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "attach: " + fragment); + if (fragment.mDetached) { + fragment.mDetached = false; + if (!fragment.mAdded) { + if (mAdded == null) { + mAdded = new ArrayList(); + } + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + if (DEBUG) Log.v(TAG, "add from attach: " + fragment); + mAdded.add(fragment); + fragment.mAdded = true; + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + moveToState(fragment, mCurState, transition, transitionStyle, false); + } + } + } + + public Fragment findFragmentById(int id) { + if (mAdded != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + if (mActive != null) { + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByTag(String tag) { + if (mAdded != null && tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + if (mActive != null && tag != null) { + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(String who) { + if (mActive != null && who != null) { + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && (f=f.findFragmentByWho(who)) != null) { + return f; + } + } + } + return null; + } + + private void checkStateLoss() { + if (mStateSaved) { + throw new IllegalStateException( + "Can not perform this action after onSaveInstanceState"); + } + if (mNoTransactionsBecause != null) { + throw new IllegalStateException( + "Can not perform this action inside of " + mNoTransactionsBecause); + } + } + + /** + * Adds an action to the queue of pending actions. + * + * @param action the action to add + * @param allowStateLoss whether to allow loss of state information + * @throws IllegalStateException if the activity has been destroyed + */ + public void enqueueAction(Runnable action, boolean allowStateLoss) { + if (!allowStateLoss) { + checkStateLoss(); + } + synchronized (this) { + if (mDestroyed || mActivity == null) { + throw new IllegalStateException("Activity has been destroyed"); + } + if (mPendingActions == null) { + mPendingActions = new ArrayList(); + } + mPendingActions.add(action); + if (mPendingActions.size() == 1) { + mActivity.mHandler.removeCallbacks(mExecCommit); + mActivity.mHandler.post(mExecCommit); + } + } + } + + public int allocBackStackIndex(BackStackRecord bse) { + synchronized (this) { + if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int index = mBackStackIndices.size(); + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.add(bse); + return index; + + } else { + int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1); + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.set(index, bse); + return index; + } + } + } + + public void setBackStackIndex(int index, BackStackRecord bse) { + synchronized (this) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int N = mBackStackIndices.size(); + if (index < N) { + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.set(index, bse); + } else { + while (N < index) { + mBackStackIndices.add(null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Adding available back stack index " + N); + mAvailBackStackIndices.add(N); + N++; + } + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.add(bse); + } + } + } + + public void freeBackStackIndex(int index) { + synchronized (this) { + mBackStackIndices.set(index, null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Freeing back stack index " + index); + mAvailBackStackIndices.add(index); + } + } + + /** + * Only call from main thread! + */ + public boolean execPendingActions() { + if (mExecutingActions) { + throw new IllegalStateException("Recursive entry to executePendingTransactions"); + } + + if (Looper.myLooper() != mActivity.mHandler.getLooper()) { + throw new IllegalStateException("Must be called from main thread of process"); + } + + boolean didSomething = false; + + while (true) { + int numActions; + + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + break; + } + + numActions = mPendingActions.size(); + if (mTmpActions == null || mTmpActions.length < numActions) { + mTmpActions = new Runnable[numActions]; + } + mPendingActions.toArray(mTmpActions); + mPendingActions.clear(); + mActivity.mHandler.removeCallbacks(mExecCommit); + } + + mExecutingActions = true; + for (int i=0; i(); + } + mBackStack.add(state); + reportBackStackChanged(); + } + + boolean popBackStackState(Handler handler, String name, int id, int flags) { + if (mBackStack == null) { + return false; + } + if (name == null && id < 0 && (flags&POP_BACK_STACK_INCLUSIVE) == 0) { + int last = mBackStack.size()-1; + if (last < 0) { + return false; + } + final BackStackRecord bss = mBackStack.remove(last); + SparseArray firstOutFragments = new SparseArray(); + SparseArray lastInFragments = new SparseArray(); + bss.calculateBackFragments(firstOutFragments, lastInFragments); + bss.popFromBackStack(true, null, firstOutFragments, lastInFragments); + reportBackStackChanged(); + } else { + int index = -1; + if (name != null || id >= 0) { + // If a name or ID is specified, look for that place in + // the stack. + index = mBackStack.size()-1; + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if (name != null && name.equals(bss.getName())) { + break; + } + if (id >= 0 && id == bss.mIndex) { + break; + } + index--; + } + if (index < 0) { + return false; + } + if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) { + index--; + // Consume all following entries that match. + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if ((name != null && name.equals(bss.getName())) + || (id >= 0 && id == bss.mIndex)) { + index--; + continue; + } + break; + } + } + } + if (index == mBackStack.size()-1) { + return false; + } + final ArrayList states + = new ArrayList(); + for (int i=mBackStack.size()-1; i>index; i--) { + states.add(mBackStack.remove(i)); + } + final int LAST = states.size()-1; + SparseArray firstOutFragments = new SparseArray(); + SparseArray lastInFragments = new SparseArray(); + for (int i=0; i<=LAST; i++) { + states.get(i).calculateBackFragments(firstOutFragments, lastInFragments); + } + BackStackRecord.TransitionState state = null; + for (int i=0; i<=LAST; i++) { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i)); + state = states.get(i).popFromBackStack(i == LAST, state, + firstOutFragments, lastInFragments); + } + reportBackStackChanged(); + } + return true; + } + + ArrayList retainNonConfig() { + ArrayList fragments = null; + if (mActive != null) { + for (int i=0; i(); + } + fragments.add(f); + f.mRetaining = true; + f.mTargetIndex = f.mTarget != null ? f.mTarget.mIndex : -1; + if (DEBUG) Log.v(TAG, "retainNonConfig: keeping retained " + f); + } + } + } + return fragments; + } + + void saveFragmentViewState(Fragment f) { + if (f.mInnerView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray(); + } else { + mStateArray.clear(); + } + f.mInnerView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Bundle saveFragmentBasicState(Fragment f) { + Bundle result = null; + + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.performSaveInstanceState(mStateBundle); + if (!mStateBundle.isEmpty()) { + result = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + } + if (f.mSavedViewState != null) { + if (result == null) { + result = new Bundle(); + } + result.putSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState); + } + if (!f.mUserVisibleHint) { + if (result == null) { + result = new Bundle(); + } + // Only add this if it's not the default value + result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint); + } + + return result; + } + + Parcelable saveAllState() { + // Make sure all pending operations have now been executed to get + // our state update-to-date. + execPendingActions(); + + if (HONEYCOMB) { + // As of Honeycomb, we save state after pausing. Prior to that + // it is before pausing. With fragments this is an issue, since + // there are many things you may do after pausing but before + // stopping that change the fragment state. For those older + // devices, we will not at this point say that we have saved + // the state, so we will allow them to continue doing fragment + // transactions. This retains the same semantics as Honeycomb, + // though you do have the risk of losing the very most recent state + // if the process is killed... we'll live with that. + mStateSaved = true; + } + + if (mActive == null || mActive.size() <= 0) { + return null; + } + + // First collect all active fragments. + int N = mActive.size(); + FragmentState[] active = new FragmentState[N]; + boolean haveFragments = false; + for (int i=0; i Fragment.INITIALIZING && fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = saveFragmentBasicState(f); + + if (f.mTarget != null) { + if (f.mTarget.mIndex < 0) { + throwException(new IllegalStateException( + "Failure saving state: " + f + + " has target not in fragment manager: " + f.mTarget)); + } + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + putFragment(fs.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget); + if (f.mTargetRequestCode != 0) { + fs.mSavedFragmentState.putInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, + f.mTargetRequestCode); + } + } + + } else { + fs.mSavedFragmentState = f.mSavedFragmentState; + } + + if (DEBUG) Log.v(TAG, "Saved state of " + f + ": " + + fs.mSavedFragmentState); + } + } + + if (!haveFragments) { + if (DEBUG) Log.v(TAG, "saveAllState: no fragments!"); + return null; + } + + int[] added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + if (mAdded != null) { + N = mAdded.size(); + if (N > 0) { + added = new int[N]; + for (int i=0; i 0) { + backStack = new BackStackState[N]; + for (int i=0; i nonConfig) { + // If there is no saved state at all, then there can not be + // any nonConfig fragments either, so that is that. + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + if (nonConfig != null) { + for (int i=0; i(fms.mActive.length); + if (mAvailIndices != null) { + mAvailIndices.clear(); + } + for (int i=0; i(); + } + if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i); + mAvailIndices.add(i); + } + } + + // Update the target of all retained fragments. + if (nonConfig != null) { + for (int i=0; i= 0) { + if (f.mTargetIndex < mActive.size()) { + f.mTarget = mActive.get(f.mTargetIndex); + } else { + Log.w(TAG, "Re-attaching retained fragment " + f + + " target no longer exists: " + f.mTargetIndex); + f.mTarget = null; + } + } + } + } + + // Build the list of currently added fragments. + if (fms.mAdded != null) { + mAdded = new ArrayList(fms.mAdded.length); + for (int i=0; i(fms.mBackStack.length); + for (int i=0; i= 0) { + setBackStackIndex(bse.mIndex, bse); + } + } + } else { + mBackStack = null; + } + } + + public void attachActivity(FragmentActivity activity, + FragmentContainer container, Fragment parent) { + if (mActivity != null) throw new IllegalStateException("Already attached"); + mActivity = activity; + mContainer = container; + mParent = parent; + } + + public void noteStateNotSaved() { + mStateSaved = false; + } + + public void dispatchCreate() { + mStateSaved = false; + moveToState(Fragment.CREATED, false); + } + + public void dispatchActivityCreated() { + mStateSaved = false; + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchStart() { + mStateSaved = false; + moveToState(Fragment.STARTED, false); + } + + public void dispatchResume() { + mStateSaved = false; + moveToState(Fragment.RESUMED, false); + } + + public void dispatchPause() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchStop() { + // See saveAllState() for the explanation of this. We do this for + // all platform versions, to keep our behavior more consistent between + // them. + mStateSaved = true; + + moveToState(Fragment.STOPPED, false); + } + + public void dispatchReallyStop() { + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchDestroyView() { + moveToState(Fragment.CREATED, false); + } + + public void dispatchDestroy() { + mDestroyed = true; + execPendingActions(); + moveToState(Fragment.INITIALIZING, false); + mActivity = null; + mContainer = null; + mParent = null; + } + + public void dispatchConfigurationChanged(Configuration newConfig) { + if (mAdded != null) { + for (int i=0; i newMenus = null; + if (mAdded != null) { + for (int i=0; i(); + } + newMenus.add(f); + } + } + } + } + + if (mCreatedMenus != null) { + for (int i=0; iThis version of the pager is best for use when there are a handful of + * typically more static fragments to be paged through, such as a set of tabs. + * The fragment of each page the user visits will be kept in memory, though its + * view hierarchy may be destroyed when not visible. This can result in using + * a significant amount of memory since fragment instances can hold on to an + * arbitrary amount of state. For larger sets of pages, consider + * {@link FragmentStatePagerAdapter}. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentPagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentPagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentPagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + private Fragment mCurrentPrimaryItem = null; + + public FragmentPagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + final long itemId = getItemId(position); + + // Do we already have this fragment? + String name = makeFragmentName(container.getId(), itemId); + Fragment fragment = mFragmentManager.findFragmentByTag(name); + if (fragment != null) { + if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); + mCurTransaction.attach(fragment); + } else { + fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); + mCurTransaction.add(container.getId(), fragment, + makeFragmentName(container.getId(), itemId)); + } + if (fragment != mCurrentPrimaryItem) { + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + } + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + + " v=" + ((Fragment)object).getView()); + mCurTransaction.detach((Fragment)object); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + return null; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Return a unique identifier for the item at the given position. + * + *

The default implementation returns the given position. + * Subclasses should override this method if the positions of items can change.

+ * + * @param position Position within this adapter + * @return Unique identifier for the item at position + */ + public long getItemId(int position) { + return position; + } + + private static String makeFragmentName(int viewId, long id) { + return "android:switcher:" + viewId + ":" + id; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java new file mode 100644 index 00000000000..0c3c6c5adfa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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 java.util.ArrayList; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + *

This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + f.setMenuVisibility(false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java new file mode 100644 index 00000000000..7ffabdda4fa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java @@ -0,0 +1,355 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import java.util.ArrayList; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; + +/** + * Special TabHost that allows the use of {@link Fragment} objects for + * its tab content. When placing this in a view hierarchy, after inflating + * the hierarchy you must call {@link #setup(Context, FragmentManager, int)} + * to complete the initialization of the tab host. + * + *

Here is a simple example of using a FragmentTabHost in an Activity: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabs.java + * complete} + * + *

This can also be used inside of a fragment through fragment nesting: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java + * complete} + */ +public class FragmentTabHost extends TabHost + implements TabHost.OnTabChangeListener { + private final ArrayList mTabs = new ArrayList(); + private FrameLayout mRealTabContent; + private Context mContext; + private FragmentManager mFragmentManager; + private int mContainerId; + private TabHost.OnTabChangeListener mOnTabChangeListener; + private TabInfo mLastTab; + private boolean mAttached; + + static final class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + static class SavedState extends BaseSavedState { + String curTab; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + curTab = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(curTab); + } + + @Override + public String toString() { + return "FragmentTabHost.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " curTab=" + curTab + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public FragmentTabHost(Context context) { + // Note that we call through to the version that takes an AttributeSet, + // because the simple Context construct can result in a broken object! + super(context, null); + initFragmentTabHost(context, null); + } + + public FragmentTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + initFragmentTabHost(context, attrs); + } + + private void initFragmentTabHost(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.inflatedId }, 0, 0); + mContainerId = a.getResourceId(0, 0); + a.recycle(); + + super.setOnTabChangedListener(this); + } + + private void ensureHierarchy(Context context) { + // If owner hasn't made its own view hierarchy, then as a convenience + // we will construct a standard one here. + if (findViewById(android.R.id.tabs) == null) { + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + addView(ll, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + + TabWidget tw = new TabWidget(context); + tw.setId(android.R.id.tabs); + tw.setOrientation(TabWidget.HORIZONTAL); + ll.addView(tw, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0)); + + FrameLayout fl = new FrameLayout(context); + fl.setId(android.R.id.tabcontent); + ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); + + mRealTabContent = fl = new FrameLayout(context); + mRealTabContent.setId(mContainerId); + ll.addView(fl, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.FILL_PARENT, 0, 1)); + } + } + + /** + * @deprecated Don't call the original TabHost setup, you must instead + * call {@link #setup(Context, FragmentManager)} or + * {@link #setup(Context, FragmentManager, int)}. + */ + @Override @Deprecated + public void setup() { + throw new IllegalStateException( + "Must call setup() that takes a Context and FragmentManager"); + } + + public void setup(Context context, FragmentManager manager) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + ensureContent(); + } + + public void setup(Context context, FragmentManager manager, int containerId) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + mContainerId = containerId; + ensureContent(); + mRealTabContent.setId(containerId); + + // We must have an ID to be able to save/restore our state. If + // the owner hasn't set one at this point, we will set it ourself. + if (getId() == View.NO_ID) { + setId(android.R.id.tabhost); + } + } + + private void ensureContent() { + if (mRealTabContent == null) { + mRealTabContent = (FrameLayout)findViewById(mContainerId); + if (mRealTabContent == null) { + throw new IllegalStateException( + "No tab content FrameLayout found for id " + mContainerId); + } + } + } + + @Override + public void setOnTabChangedListener(OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + + if (mAttached) { + // If we are already attached to the window, then check to make + // sure this tab's fragment is inactive if it exists. This shouldn't + // normally happen. + info.fragment = mFragmentManager.findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + FragmentTransaction ft = mFragmentManager.beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + } + + mTabs.add(info); + addTab(tabSpec); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + String currentTab = getCurrentTabTag(); + + // Go through all tabs and make sure their fragments match + // the correct state. + FragmentTransaction ft = null; + for (int i=0; i

A transaction can only be committed with this method + * prior to its containing activity saving its state. If the commit is + * attempted after that point, an exception will be thrown. This is + * because the state after the commit can be lost if the activity needs to + * be restored from its state. See {@link #commitAllowingStateLoss()} for + * situations where it may be okay to lose the commit.

+ * + * @return Returns the identifier of this transaction's back stack entry, + * if {@link #addToBackStack(String)} had been called. Otherwise, returns + * a negative number. + */ + public abstract int commit(); + + /** + * Like {@link #commit} but allows the commit to be executed after an + * activity's state is saved. This is dangerous because the commit can + * be lost if the activity needs to later be restored from its state, so + * this should only be used for cases where it is okay for the UI state + * to change unexpectedly on the user. + */ + public abstract int commitAllowingStateLoss(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java new file mode 100644 index 00000000000..ef41045c1ae --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java @@ -0,0 +1,383 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.ArrayMap; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class FragmentTransitionCompat21 { + public static String getTransitionName(View view) { + return view.getTransitionName(); + } + + public static Object cloneTransition(Object transition) { + if (transition != null) { + transition = ((Transition)transition).clone(); + } + return transition; + } + + public static Object captureExitingViews(Object exitTransition, View root, + ArrayList viewList, Map namedViews, View nonExistentView) { + if (exitTransition != null) { + captureTransitioningViews(viewList, root); + if (namedViews != null) { + viewList.removeAll(namedViews.values()); + } + if (viewList.isEmpty()) { + exitTransition = null; + } else { + viewList.add(nonExistentView); + addTargets((Transition) exitTransition, viewList); + } + } + return exitTransition; + } + + public static void excludeTarget(Object transitionObject, View view, boolean exclude) { + Transition transition = (Transition) transitionObject; + transition.excludeTarget(view, exclude); + } + + public static void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject) { + Transition transition = (Transition) transitionObject; + TransitionManager.beginDelayedTransition(sceneRoot, transition); + } + + public static void setEpicenter(Object transitionObject, View view) { + Transition transition = (Transition) transitionObject; + final Rect epicenter = getBoundsOnScreen(view); + + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + return epicenter; + } + }); + } + + /** + * Prepares the enter transition by adding a non-existent view to the transition's target list + * and setting it epicenter callback. By adding a non-existent view to the target list, + * we can prevent any view from being targeted at the beginning of the transition. + * We will add to the views before the end state of the transition is captured so that the + * views will appear. At the start of the transition, we clear the list of targets so that + * we can restore the state of the transition and use it again. + * + *

The shared element transition maps its shared elements immediately prior to + * capturing the final state of the Transition.

+ */ + public static void addTransitionTargets(Object enterTransitionObject, + Object sharedElementTransitionObject, final View container, + final ViewRetriever inFragment, final View nonExistentView, + EpicenterView epicenterView, final Map nameOverrides, + final ArrayList enteringViews, final Map renamedViews, + final ArrayList sharedElementTargets) { + if (enterTransitionObject != null || sharedElementTransitionObject != null) { + final Transition enterTransition = (Transition) enterTransitionObject; + if (enterTransition != null) { + enterTransition.addTarget(nonExistentView); + } + if (sharedElementTransitionObject != null) { + Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + addTargets(sharedElementTransition, sharedElementTargets); + } + + if (inFragment != null) { + container.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + container.getViewTreeObserver().removeOnPreDrawListener(this); + View fragmentView = inFragment.getView(); + if (fragmentView != null) { + if (!nameOverrides.isEmpty()) { + findNamedViews(renamedViews, fragmentView); + renamedViews.keySet().retainAll(nameOverrides.values()); + for (Map.Entry entry : nameOverrides.entrySet()) { + String to = entry.getValue(); + View view = renamedViews.get(to); + if (view != null) { + String from = entry.getKey(); + view.setTransitionName(from); + } + } + } + if (enterTransition != null) { + captureTransitioningViews(enteringViews, fragmentView); + enteringViews.removeAll(renamedViews.values()); + enteringViews.add(nonExistentView); + enterTransition.removeTarget(nonExistentView); + addTargets(enterTransition, enteringViews); + } + } + return true; + } + }); + } + setSharedElementEpicenter(enterTransition, epicenterView); + } + } + + public static Object mergeTransitions(Object enterTransitionObject, + Object exitTransitionObject, Object sharedElementTransitionObject, + boolean allowOverlap) { + boolean overlap = true; + Transition enterTransition = (Transition) enterTransitionObject; + Transition exitTransition = (Transition) exitTransitionObject; + Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + + if (enterTransition != null && exitTransition != null) { + overlap = allowOverlap; + } + + // Wrap the transitions. Explicit targets like in enter and exit will cause the + // views to be targeted regardless of excluded views. If that happens, then the + // excluded fragments views (hidden fragments) will still be in the transition. + + Transition transition; + if (overlap) { + // Regular transition -- do it all together + TransitionSet transitionSet = new TransitionSet(); + if (enterTransition != null) { + transitionSet.addTransition(enterTransition); + } + if (exitTransition != null) { + transitionSet.addTransition(exitTransition); + } + if (sharedElementTransition != null) { + transitionSet.addTransition(sharedElementTransition); + } + transition = transitionSet; + } else { + // First do exit, then enter, but allow shared element transition to happen + // during both. + Transition staggered = null; + if (exitTransition != null && enterTransition != null) { + staggered = new TransitionSet() + .addTransition(exitTransition) + .addTransition(enterTransition) + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); + } else if (exitTransition != null) { + staggered = exitTransition; + } else if (enterTransition != null) { + staggered = enterTransition; + } + if (sharedElementTransition != null) { + TransitionSet together = new TransitionSet(); + if (staggered != null) { + together.addTransition(staggered); + } + together.addTransition(sharedElementTransition); + transition = together; + } else { + transition = staggered; + } + } + return transition; + } + + + + private static void setSharedElementEpicenter(Transition transition, + final EpicenterView epicenterView) { + if (transition != null) { + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + private Rect mEpicenter; + + @Override + public Rect onGetEpicenter(Transition transition) { + if (mEpicenter == null && epicenterView.epicenter != null) { + mEpicenter = getBoundsOnScreen(epicenterView.epicenter); + } + return mEpicenter; + } + }); + } + } + + private static Rect getBoundsOnScreen(View view) { + Rect epicenter = new Rect(); + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + // not as good as View.getBoundsOnScreen, but that's not public + epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); + return epicenter; + } + + private static void captureTransitioningViews(ArrayList transitioningViews, View view) { + if (view.getVisibility() == View.VISIBLE) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + if (viewGroup.isTransitionGroup()) { + transitioningViews.add(viewGroup); + } else { + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + captureTransitioningViews(transitioningViews, child); + } + } + } else { + transitioningViews.add(view); + } + } + } + + public static void findNamedViews(Map namedViews, View view) { + if (view.getVisibility() == View.VISIBLE) { + String transitionName = view.getTransitionName(); + if (transitionName != null) { + namedViews.put(transitionName, view); + } + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + findNamedViews(namedViews, child); + } + } + } + } + + public static void cleanupTransitions(final View sceneRoot, final View nonExistentView, + Object enterTransitionObject, final ArrayList enteringViews, + Object exitTransitionObject, final ArrayList exitingViews, + Object sharedElementTransitionObject, final ArrayList sharedElementTargets, + Object overallTransitionObject, final ArrayList hiddenViews, + final Map renamedViews) { + final Transition enterTransition = (Transition) enterTransitionObject; + final Transition exitTransition = (Transition) exitTransitionObject; + final Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + final Transition overallTransition = (Transition) overallTransitionObject; + if (overallTransition != null) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + if (enterTransition != null) { + enterTransition.removeTarget(nonExistentView); + removeTargets(enterTransition, enteringViews); + } + if (exitTransition != null) { + removeTargets(exitTransition, exitingViews); + } + if (sharedElementTransition != null) { + removeTargets(sharedElementTransition, sharedElementTargets); + } + for (Map.Entry entry : renamedViews.entrySet()) { + View view = entry.getValue(); + String name = entry.getKey(); + view.setTransitionName(name); + } + int numViews = hiddenViews.size(); + for (int i = 0; i < numViews; i++) { + overallTransition.excludeTarget(hiddenViews.get(i), false); + } + overallTransition.excludeTarget(nonExistentView, false); + return true; + } + }); + } + } + + /** + * This method removes the views from transitions that target ONLY those views. + * The views list should match those added in addTargets and should contain + * one view that is not in the view hierarchy (state.nonExistentView). + */ + public static void removeTargets(Object transitionObject, ArrayList views) { + Transition transition = (Transition) transitionObject; + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + removeTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (targets != null && targets.size() == views.size() && + targets.containsAll(views)) { + // We have an exact match. We must have added these earlier in addTargets + for (int i = views.size() - 1; i >= 0; i--) { + transition.removeTarget(views.get(i)); + } + } + } + } + + /** + * This method adds views as targets to the transition, but only if the transition + * doesn't already have a target. It is best for views to contain one View object + * that does not exist in the view hierarchy (state.nonExistentView) so that + * when they are removed later, a list match will suffice to remove the targets. + * Otherwise, if you happened to have targeted the exact views for the transition, + * the removeTargets call will remove them unexpectedly. + */ + public static void addTargets(Object transitionObject, ArrayList views) { + Transition transition = (Transition) transitionObject; + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + addTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (isNullOrEmpty(targets)) { + // We can just add the target views + int numViews = views.size(); + for (int i = 0; i < numViews; i++) { + transition.addTarget(views.get(i)); + } + } + } + } + + private static boolean hasSimpleTarget(Transition transition) { + return !isNullOrEmpty(transition.getTargetIds()) || + !isNullOrEmpty(transition.getTargetNames()) || + !isNullOrEmpty(transition.getTargetTypes()); + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + + public interface ViewRetriever { + View getView(); + } + + public static class EpicenterView { + public View epicenter; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl b/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl new file mode 100644 index 00000000000..9df1577eae9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; + +/** + * Interface used for delivering notifications via a side channel that bypasses + * the NotificationManagerService. + * + * @hide + */ +oneway interface INotificationSideChannel { + /** + * Send an ambient notification to the service. + */ + void notify(String packageName, int id, String tag, in Notification notification); + + /** + * Cancel an already-notified notification. + */ + void cancel(String packageName, int id, String tag); + + /** + * Cancel all notifications for the given package. + */ + void cancelAll(String packageName); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java new file mode 100644 index 00000000000..1ddea1c7906 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Static library support version of the framework's {@link android.app.ListFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class ListFragment extends Fragment { + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView)parent, v, position, id); + } + }; + + ListAdapter mAdapter; + ListView mList; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mListContainer; + CharSequence mEmptyText; + boolean mListShown; + + public ListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses + * can override to replace with their own layout. If doing so, the + * returned view hierarchy must have a ListView whose id + * is {@link android.R.id#list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id#empty android.R.id.empty} + * that is to be shown when the list is empty. + * + *

If you are overriding this method with your own custom content, + * consider including the standard layout {@link android.R.layout#list_content} + * in your layout file, so that you continue to retain all of the standard + * behavior of ListFragment. In particular, this is currently the only + * way to have the built-in indeterminant progress state be shown. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final Context context = getActivity(); + + FrameLayout root = new FrameLayout(context); + + // ------------------------------------------------------------------ + + LinearLayout pframe = new LinearLayout(context); + pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); + pframe.setOrientation(LinearLayout.VERTICAL); + pframe.setVisibility(View.GONE); + pframe.setGravity(Gravity.CENTER); + + ProgressBar progress = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + pframe.addView(progress, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(pframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + FrameLayout lframe = new FrameLayout(context); + lframe.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView tv = new TextView(getActivity()); + tv.setId(INTERNAL_EMPTY_ID); + tv.setGravity(Gravity.CENTER); + lframe.addView(tv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + ListView lv = new ListView(getActivity()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + lframe.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + return root; + } + + /** + * Attach to list view once the view hierarchy has been created. + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + mListShown = false; + mEmptyView = mProgressContainer = mListContainer = null; + mStandardEmptyView = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. + * Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(ListView l, View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(ListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mList != null) { + mList.setAdapter(adapter); + if (!mListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, getView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified + * position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mList.getSelectedItemId(); + } + + /** + * Get the activity's list view widget. + */ + public ListView getListView() { + ensureList(); + return mList; + } + + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mStandardEmptyView.setText(text); + if (mEmptyText == null) { + mList.setEmptyView(mStandardEmptyView); + } + mEmptyText = text; + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + *

Applications do not normally need to use this themselves. The default + * behavior of ListFragment is to start with the list not being shown, only + * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}. + * If the list at that point had not been shown, when it does get shown + * it will be do without the user ever seeing the hidden state. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when + * transitioning from the previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ListAdapter getListAdapter() { + return mAdapter; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById(INTERNAL_EMPTY_ID); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } else { + mStandardEmptyView.setVisibility(View.GONE); + } + mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + mListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + if (rawListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } else if (mEmptyText != null) { + mStandardEmptyView.setText(mEmptyText); + mList.setEmptyView(mStandardEmptyView); + } + } + mListShown = true; + mList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + ListAdapter adapter = mAdapter; + mAdapter = null; + setListAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java b/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java new file mode 100644 index 00000000000..57c0a30158c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.os.Bundle; +import android.support.v4.content.Loader; +import android.support.v4.util.DebugUtils; +import android.support.v4.util.SparseArrayCompat; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.reflect.Modifier; + +/** + * Static library support version of the framework's {@link android.app.LoaderManager}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + * + *

Your activity must derive from {@link FragmentActivity} to use this. + */ +public abstract class LoaderManager { + /** + * Callback interface for a client to interact with the manager. + */ + public interface LoaderCallbacks { + /** + * Instantiate and return a new Loader for the given ID. + * + * @param id The ID whose loader is to be created. + * @param args Any arguments supplied by the caller. + * @return Return a new Loader instance that is ready to start loading. + */ + public Loader onCreateLoader(int id, Bundle args); + + /** + * Called when a previously created loader has finished its load. Note + * that normally an application is not allowed to commit fragment + * transactions while in this call, since it can happen after an + * activity's state is saved. See {@link FragmentManager#beginTransaction() + * FragmentManager.openTransaction()} for further discussion on this. + * + *

This function is guaranteed to be called prior to the release of + * the last data that was supplied for this Loader. At this point + * you should remove all use of the old data (since it will be released + * soon), but should not do your own release of the data since its Loader + * owns it and will take care of that. The Loader will take care of + * management of its data so you don't have to. In particular: + * + *

    + *
  • The Loader will monitor for changes to the data, and report + * them to you through new calls here. You should not monitor the + * data yourself. For example, if the data is a {@link android.database.Cursor} + * and you place it in a {@link android.widget.CursorAdapter}, use + * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context, + * android.database.Cursor, int)} constructor without passing + * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY} + * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} + * (that is, use 0 for the flags argument). This prevents the CursorAdapter + * from doing its own observing of the Cursor, which is not needed since + * when a change happens you will get a new Cursor throw another call + * here. + *

  • The Loader will release the data once it knows the application + * is no longer using it. For example, if the data is + * a {@link android.database.Cursor} from a {@link android.content.CursorLoader}, + * you should not call close() on it yourself. If the Cursor is being placed in a + * {@link android.widget.CursorAdapter}, you should use the + * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)} + * method so that the old Cursor is not closed. + *
+ * + * @param loader The Loader that has finished. + * @param data The data generated by the Loader. + */ + public void onLoadFinished(Loader loader, D data); + + /** + * Called when a previously created loader is being reset, and thus + * making its data unavailable. The application should at this point + * remove any references it has to the Loader's data. + * + * @param loader The Loader that is being reset. + */ + public void onLoaderReset(Loader loader); + } + + /** + * Ensures a loader is initialized and active. If the loader doesn't + * already exist, one is created and (if the activity/fragment is currently + * started) starts the loader. Otherwise the last created + * loader is re-used. + * + *

In either case, the given callback is associated with the loader, and + * will be called as the loader state changes. If at the point of call + * the caller is in its started state, and the requested loader + * already exists and has generated its data, then + * callback {@link LoaderCallbacks#onLoadFinished} will + * be called immediately (inside of this function), so you must be prepared + * for this to happen. + * + * @param id A unique identifier for this loader. Can be whatever you want. + * Identifiers are scoped to a particular LoaderManager instance. + * @param args Optional arguments to supply to the loader at construction. + * If a loader already exists (a new one does not need to be created), this + * parameter will be ignored and the last arguments continue to be used. + * @param callback Interface the LoaderManager will call to report about + * changes in the state of the loader. Required. + */ + public abstract Loader initLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Starts a new or restarts an existing {@link android.content.Loader} in + * this manager, registers the callbacks to it, + * and (if the activity/fragment is currently started) starts loading it. + * If a loader with the same id has previously been + * started it will automatically be destroyed when the new loader completes + * its work. The callback will be delivered before the old loader + * is destroyed. + * + * @param id A unique identifier for this loader. Can be whatever you want. + * Identifiers are scoped to a particular LoaderManager instance. + * @param args Optional arguments to supply to the loader at construction. + * @param callback Interface the LoaderManager will call to report about + * changes in the state of the loader. Required. + */ + public abstract Loader restartLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Stops and removes the loader with the given ID. If this loader + * had previously reported data to the client through + * {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call + * will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}. + */ + public abstract void destroyLoader(int id); + + /** + * Return the Loader with the given id or null if no matching Loader + * is found. + */ + public abstract Loader getLoader(int id); + + /** + * Print the LoaderManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args); + + /** + * Control whether the framework's internal loader manager debugging + * logs are turned on. If enabled, you will see output in logcat as + * the framework performs loader operations. + */ + public static void enableDebugLogging(boolean enabled) { + LoaderManagerImpl.DEBUG = enabled; + } + + /** + * Returns true if any loaders managed are currently running and have not + * returned data to the application yet. + */ + public boolean hasRunningLoaders() { return false; } +} + +class LoaderManagerImpl extends LoaderManager { + static final String TAG = "LoaderManager"; + static boolean DEBUG = false; + + // These are the currently active loaders. A loader is here + // from the time its load is started until it has been explicitly + // stopped or restarted by the application. + final SparseArrayCompat mLoaders = new SparseArrayCompat(); + + // These are previously run loaders. This list is maintained internally + // to avoid destroying a loader while an application is still using it. + // It allows an application to restart a loader, but continue using its + // previously run loader until the new loader's data is available. + final SparseArrayCompat mInactiveLoaders = new SparseArrayCompat(); + + final String mWho; + + FragmentActivity mActivity; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + + boolean mCreatingLoader; + + final class LoaderInfo implements Loader.OnLoadCompleteListener { + final int mId; + final Bundle mArgs; + LoaderManager.LoaderCallbacks mCallbacks; + Loader mLoader; + boolean mHaveData; + boolean mDeliveredData; + Object mData; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + boolean mReportNextStart; + boolean mDestroyed; + boolean mListenerRegistered; + + LoaderInfo mPendingLoader; + + public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks callbacks) { + mId = id; + mArgs = args; + mCallbacks = callbacks; + } + + void start() { + if (mRetaining && mRetainingStarted) { + // Our owner is started, but we were being retained from a + // previous instance in the started state... so there is really + // nothing to do here, since the loaders are still started. + mStarted = true; + return; + } + + if (mStarted) { + // If loader already started, don't restart. + return; + } + + mStarted = true; + + if (DEBUG) Log.v(TAG, " Starting: " + this); + if (mLoader == null && mCallbacks != null) { + mLoader = mCallbacks.onCreateLoader(mId, mArgs); + } + if (mLoader != null) { + if (mLoader.getClass().isMemberClass() + && !Modifier.isStatic(mLoader.getClass().getModifiers())) { + throw new IllegalArgumentException( + "Object returned from onCreateLoader must not be a non-static inner member class: " + + mLoader); + } + if (!mListenerRegistered) { + mLoader.registerListener(mId, this); + mListenerRegistered = true; + } + mLoader.startLoading(); + } + } + + void retain() { + if (DEBUG) Log.v(TAG, " Retaining: " + this); + mRetaining = true; + mRetainingStarted = mStarted; + mStarted = false; + mCallbacks = null; + } + + void finishRetain() { + if (mRetaining) { + if (DEBUG) Log.v(TAG, " Finished Retaining: " + this); + mRetaining = false; + if (mStarted != mRetainingStarted) { + if (!mStarted) { + // This loader was retained in a started state, but + // at the end of retaining everything our owner is + // no longer started... so make it stop. + stop(); + } + } + } + + if (mStarted && mHaveData && !mReportNextStart) { + // This loader has retained its data, either completely across + // a configuration change or just whatever the last data set + // was after being restarted from a stop, and now at the point of + // finishing the retain we find we remain started, have + // our data, and the owner has a new callback... so + // let's deliver the data now. + callOnLoadFinished(mLoader, mData); + } + } + + void reportStart() { + if (mStarted) { + if (mReportNextStart) { + mReportNextStart = false; + if (mHaveData) { + callOnLoadFinished(mLoader, mData); + } + } + } + } + + void stop() { + if (DEBUG) Log.v(TAG, " Stopping: " + this); + mStarted = false; + if (!mRetaining) { + if (mLoader != null && mListenerRegistered) { + // Let the loader know we're done with it + mListenerRegistered = false; + mLoader.unregisterListener(this); + mLoader.stopLoading(); + } + } + } + + void destroy() { + if (DEBUG) Log.v(TAG, " Destroying: " + this); + mDestroyed = true; + boolean needReset = mDeliveredData; + mDeliveredData = false; + if (mCallbacks != null && mLoader != null && mHaveData && needReset) { + if (DEBUG) Log.v(TAG, " Reseting: " + this); + String lastBecause = null; + if (mActivity != null) { + lastBecause = mActivity.mFragments.mNoTransactionsBecause; + mActivity.mFragments.mNoTransactionsBecause = "onLoaderReset"; + } + try { + mCallbacks.onLoaderReset(mLoader); + } finally { + if (mActivity != null) { + mActivity.mFragments.mNoTransactionsBecause = lastBecause; + } + } + } + mCallbacks = null; + mData = null; + mHaveData = false; + if (mLoader != null) { + if (mListenerRegistered) { + mListenerRegistered = false; + mLoader.unregisterListener(this); + } + mLoader.reset(); + } + if (mPendingLoader != null) { + mPendingLoader.destroy(); + } + } + + @Override public void onLoadComplete(Loader loader, Object data) { + if (DEBUG) Log.v(TAG, "onLoadComplete: " + this); + + if (mDestroyed) { + if (DEBUG) Log.v(TAG, " Ignoring load complete -- destroyed"); + return; + } + + if (mLoaders.get(mId) != this) { + // This data is not coming from the current active loader. + // We don't care about it. + if (DEBUG) Log.v(TAG, " Ignoring load complete -- not active"); + return; + } + + LoaderInfo pending = mPendingLoader; + if (pending != null) { + // There is a new request pending and we were just + // waiting for the old one to complete before starting + // it. So now it is time, switch over to the new loader. + if (DEBUG) Log.v(TAG, " Switching to pending loader: " + pending); + mPendingLoader = null; + mLoaders.put(mId, null); + destroy(); + installLoader(pending); + return; + } + + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + if (mData != data || !mHaveData) { + mData = data; + mHaveData = true; + if (mStarted) { + callOnLoadFinished(loader, data); + } + } + + //if (DEBUG) Log.v(TAG, " onLoadFinished returned: " + this); + + // We have now given the application the new loader with its + // loaded data, so it should have stopped using the previous + // loader. If there is a previous loader on the inactive list, + // clean it up. + LoaderInfo info = mInactiveLoaders.get(mId); + if (info != null && info != this) { + info.mDeliveredData = false; + info.destroy(); + mInactiveLoaders.remove(mId); + } + + if (mActivity != null && !hasRunningLoaders()) { + mActivity.mFragments.startPendingDeferredFragments(); + } + } + + void callOnLoadFinished(Loader loader, Object data) { + if (mCallbacks != null) { + String lastBecause = null; + if (mActivity != null) { + lastBecause = mActivity.mFragments.mNoTransactionsBecause; + mActivity.mFragments.mNoTransactionsBecause = "onLoadFinished"; + } + try { + if (DEBUG) Log.v(TAG, " onLoadFinished in " + loader + ": " + + loader.dataToString(data)); + mCallbacks.onLoadFinished(loader, data); + } finally { + if (mActivity != null) { + mActivity.mFragments.mNoTransactionsBecause = lastBecause; + } + } + mDeliveredData = true; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("LoaderInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + sb.append(mId); + sb.append(" : "); + DebugUtils.buildShortClassTag(mLoader, sb); + sb.append("}}"); + return sb.toString(); + } + + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mId="); writer.print(mId); + writer.print(" mArgs="); writer.println(mArgs); + writer.print(prefix); writer.print("mCallbacks="); writer.println(mCallbacks); + writer.print(prefix); writer.print("mLoader="); writer.println(mLoader); + if (mLoader != null) { + mLoader.dump(prefix + " ", fd, writer, args); + } + if (mHaveData || mDeliveredData) { + writer.print(prefix); writer.print("mHaveData="); writer.print(mHaveData); + writer.print(" mDeliveredData="); writer.println(mDeliveredData); + writer.print(prefix); writer.print("mData="); writer.println(mData); + } + writer.print(prefix); writer.print("mStarted="); writer.print(mStarted); + writer.print(" mReportNextStart="); writer.print(mReportNextStart); + writer.print(" mDestroyed="); writer.println(mDestroyed); + writer.print(prefix); writer.print("mRetaining="); writer.print(mRetaining); + writer.print(" mRetainingStarted="); writer.print(mRetainingStarted); + writer.print(" mListenerRegistered="); writer.println(mListenerRegistered); + if (mPendingLoader != null) { + writer.print(prefix); writer.println("Pending Loader "); + writer.print(mPendingLoader); writer.println(":"); + mPendingLoader.dump(prefix + " ", fd, writer, args); + } + } + } + + LoaderManagerImpl(String who, FragmentActivity activity, boolean started) { + mWho = who; + mActivity = activity; + mStarted = started; + } + + void updateActivity(FragmentActivity activity) { + mActivity = activity; + } + + private LoaderInfo createLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback) { + LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks)callback); + Loader loader = callback.onCreateLoader(id, args); + info.mLoader = (Loader)loader; + return info; + } + + private LoaderInfo createAndInstallLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback) { + try { + mCreatingLoader = true; + LoaderInfo info = createLoader(id, args, callback); + installLoader(info); + return info; + } finally { + mCreatingLoader = false; + } + } + + void installLoader(LoaderInfo info) { + mLoaders.put(info.mId, info); + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), + // so only start them here if we're past that point of the activitiy's + // life cycle + info.start(); + } + } + + /** + * Call to initialize a particular ID with a Loader. If this ID already + * has a Loader associated with it, it is left unchanged and any previous + * callbacks replaced with the newly provided ones. If there is not currently + * a Loader for the ID, a new one is created and started. + * + *

This function should generally be used when a component is initializing, + * to ensure that a Loader it relies on is created. This allows it to re-use + * an existing Loader's data if there already is one, so that for example + * when an {@link Activity} is re-created after a configuration change it + * does not need to re-create its loaders. + * + *

Note that in the case where an existing Loader is re-used, the + * args given here will be ignored because you will + * continue using the previous Loader. + * + * @param id A unique (to this LoaderManager instance) identifier under + * which to manage the new Loader. + * @param args Optional arguments that will be propagated to + * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}. + * @param callback Interface implementing management of this Loader. Required. + * Its onCreateLoader() method will be called while inside of the function to + * instantiate the Loader object. + */ + @SuppressWarnings("unchecked") + public Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo info = mLoaders.get(id); + + if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args); + + if (info == null) { + // Loader doesn't already exist; create. + info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + if (DEBUG) Log.v(TAG, " Created new loader " + info); + } else { + if (DEBUG) Log.v(TAG, " Re-using existing loader " + info); + info.mCallbacks = (LoaderManager.LoaderCallbacks)callback; + } + + if (info.mHaveData && mStarted) { + // If the loader has already generated its data, report it now. + info.callOnLoadFinished(info.mLoader, info.mData); + } + + return (Loader)info.mLoader; + } + + /** + * Call to re-create the Loader associated with a particular ID. If there + * is currently a Loader associated with this ID, it will be + * canceled/stopped/destroyed as appropriate. A new Loader with the given + * arguments will be created and its data delivered to you once available. + * + *

This function does some throttling of Loaders. If too many Loaders + * have been created for the given ID but not yet generated their data, + * new calls to this function will create and return a new Loader but not + * actually start it until some previous loaders have completed. + * + *

After calling this function, any previous Loaders associated with + * this ID will be considered invalid, and you will receive no further + * data updates from them. + * + * @param id A unique (to this LoaderManager instance) identifier under + * which to manage the new Loader. + * @param args Optional arguments that will be propagated to + * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}. + * @param callback Interface implementing management of this Loader. Required. + * Its onCreateLoader() method will be called while inside of the function to + * instantiate the Loader object. + */ + @SuppressWarnings("unchecked") + public Loader restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo info = mLoaders.get(id); + if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args); + if (info != null) { + LoaderInfo inactive = mInactiveLoaders.get(id); + if (inactive != null) { + if (info.mHaveData) { + // This loader now has data... we are probably being + // called from within onLoadComplete, where we haven't + // yet destroyed the last inactive loader. So just do + // that now. + if (DEBUG) Log.v(TAG, " Removing last inactive loader: " + info); + inactive.mDeliveredData = false; + inactive.destroy(); + info.mLoader.abandon(); + mInactiveLoaders.put(id, info); + } else { + // We already have an inactive loader for this ID that we are + // waiting for! What to do, what to do... + if (!info.mStarted) { + // The current Loader has not been started... we thus + // have no reason to keep it around, so bam, slam, + // thank-you-ma'am. + if (DEBUG) Log.v(TAG, " Current loader is stopped; replacing"); + mLoaders.put(id, null); + info.destroy(); + } else { + // Now we have three active loaders... we'll queue + // up this request to be processed once one of the other loaders + // finishes. + if (info.mPendingLoader != null) { + if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader); + info.mPendingLoader.destroy(); + info.mPendingLoader = null; + } + if (DEBUG) Log.v(TAG, " Enqueuing as new pending loader"); + info.mPendingLoader = createLoader(id, args, + (LoaderManager.LoaderCallbacks)callback); + return (Loader)info.mPendingLoader.mLoader; + } + } + } else { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + if (DEBUG) Log.v(TAG, " Making last loader inactive: " + info); + info.mLoader.abandon(); + mInactiveLoaders.put(id, info); + } + } + + info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + return (Loader)info.mLoader; + } + + /** + * Rip down, tear apart, shred to pieces a current Loader ID. After returning + * from this function, any Loader objects associated with this ID are + * destroyed. Any data associated with them is destroyed. You better not + * be using it when you do this. + * @param id Identifier of the Loader to be destroyed. + */ + public void destroyLoader(int id) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id); + int idx = mLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mLoaders.valueAt(idx); + mLoaders.removeAt(idx); + info.destroy(); + } + idx = mInactiveLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mInactiveLoaders.valueAt(idx); + mInactiveLoaders.removeAt(idx); + info.destroy(); + } + if (mActivity != null && !hasRunningLoaders()) { + mActivity.mFragments.startPendingDeferredFragments(); + } + } + + /** + * Return the most recent Loader object associated with the + * given ID. + */ + @SuppressWarnings("unchecked") + public Loader getLoader(int id) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + if (loaderInfo.mPendingLoader != null) { + return (Loader)loaderInfo.mPendingLoader.mLoader; + } + return (Loader)loaderInfo.mLoader; + } + return null; + } + + void doStart() { + if (DEBUG) Log.v(TAG, "Starting in " + this); + if (mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doStart when already started: " + this, e); + return; + } + + mStarted = true; + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).start(); + } + } + + void doStop() { + if (DEBUG) Log.v(TAG, "Stopping in " + this); + if (!mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doStop when not started: " + this, e); + return; + } + + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).stop(); + } + mStarted = false; + } + + void doRetain() { + if (DEBUG) Log.v(TAG, "Retaining in " + this); + if (!mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doRetain when not started: " + this, e); + return; + } + + mRetaining = true; + mStarted = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).retain(); + } + } + + void finishRetain() { + if (mRetaining) { + if (DEBUG) Log.v(TAG, "Finished Retaining in " + this); + + mRetaining = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).finishRetain(); + } + } + } + + void doReportNextStart() { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).mReportNextStart = true; + } + } + + void doReportStart() { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).reportStart(); + } + } + + void doDestroy() { + if (!mRetaining) { + if (DEBUG) Log.v(TAG, "Destroying Active in " + this); + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).destroy(); + } + mLoaders.clear(); + } + + if (DEBUG) Log.v(TAG, "Destroying Inactive in " + this); + for (int i = mInactiveLoaders.size()-1; i >= 0; i--) { + mInactiveLoaders.valueAt(i).destroy(); + } + mInactiveLoaders.clear(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("LoaderManager{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" in "); + DebugUtils.buildShortClassTag(mActivity, sb); + sb.append("}}"); + return sb.toString(); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mLoaders.size() > 0) { + writer.print(prefix); writer.println("Active Loaders:"); + String innerPrefix = prefix + " "; + for (int i=0; i < mLoaders.size(); i++) { + LoaderInfo li = mLoaders.valueAt(i); + writer.print(prefix); writer.print(" #"); writer.print(mLoaders.keyAt(i)); + writer.print(": "); writer.println(li.toString()); + li.dump(innerPrefix, fd, writer, args); + } + } + if (mInactiveLoaders.size() > 0) { + writer.print(prefix); writer.println("Inactive Loaders:"); + String innerPrefix = prefix + " "; + for (int i=0; i < mInactiveLoaders.size(); i++) { + LoaderInfo li = mInactiveLoaders.valueAt(i); + writer.print(prefix); writer.print(" #"); writer.print(mInactiveLoaders.keyAt(i)); + writer.print(": "); writer.println(li.toString()); + li.dump(innerPrefix, fd, writer, args); + } + } + } + + @Override + public boolean hasRunningLoaders() { + boolean loadersRunning = false; + final int count = mLoaders.size(); + for (int i = 0; i < count; i++) { + final LoaderInfo li = mLoaders.valueAt(i); + loadersRunning |= li.mStarted && !li.mDeliveredData; + } + return loadersRunning; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java new file mode 100644 index 00000000000..682aaaf5e5c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java @@ -0,0 +1,309 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.support.annotation.Nullable; +import android.support.v4.content.IntentCompat; +import android.util.Log; + +/** + * NavUtils provides helper functionality for applications implementing + * recommended Android UI navigation patterns. For information about recommended + * navigation patterns see + * Tasks and Back Stack + * from the developer guide and Navigation + * from the design guide. + */ +public class NavUtils { + private static final String TAG = "NavUtils"; + public static final String PARENT_ACTIVITY = "android.support.PARENT_ACTIVITY"; + + interface NavUtilsImpl { + Intent getParentActivityIntent(Activity activity); + boolean shouldUpRecreateTask(Activity activity, Intent targetIntent); + void navigateUpTo(Activity activity, Intent upIntent); + String getParentActivityName(Context context, ActivityInfo info); + } + + static class NavUtilsImplBase implements NavUtilsImpl { + + @Override + public Intent getParentActivityIntent(Activity activity) { + String parentName = NavUtils.getParentActivityName(activity); + if (parentName == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName(activity, parentName); + try { + final String grandparent = NavUtils.getParentActivityName(activity, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } catch (NameNotFoundException e) { + Log.e(TAG, "getParentActivityIntent: bad parentActivityName '" + parentName + + "' in manifest"); + return null; + } + } + + @Override + public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + String action = activity.getIntent().getAction(); + return action != null && !action.equals(Intent.ACTION_MAIN); + } + + @Override + public void navigateUpTo(Activity activity, Intent upIntent) { + upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivity(upIntent); + activity.finish(); + } + + @Override + public String getParentActivityName(Context context, ActivityInfo info) { + if (info.metaData == null) return null; + String parentActivity = info.metaData.getString(PARENT_ACTIVITY); + if (parentActivity == null) return null; + if (parentActivity.charAt(0) == '.') { + parentActivity = context.getPackageName() + parentActivity; + } + return parentActivity; + } + } + + static class NavUtilsImplJB extends NavUtilsImplBase { + + @Override + public Intent getParentActivityIntent(Activity activity) { + // Prefer the "real" JB definition if available, + // else fall back to the meta-data element. + Intent result = NavUtilsJB.getParentActivityIntent(activity); + if (result == null) { + result = superGetParentActivityIntent(activity); + } + return result; + } + + Intent superGetParentActivityIntent(Activity activity) { + return super.getParentActivityIntent(activity); + } + + @Override + public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + return NavUtilsJB.shouldUpRecreateTask(activity, targetIntent); + } + + @Override + public void navigateUpTo(Activity activity, Intent upIntent) { + NavUtilsJB.navigateUpTo(activity, upIntent); + } + + @Override + public String getParentActivityName(Context context, ActivityInfo info) { + String result = NavUtilsJB.getParentActivityName(info); + if (result == null) { + result = super.getParentActivityName(context, info); + } + return result; + } + } + + private static final NavUtilsImpl IMPL; + + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 16) { + IMPL = new NavUtilsImplJB(); + } else { + IMPL = new NavUtilsImplBase(); + } + } + + /** + * Returns true if sourceActivity should recreate the task when navigating 'up' + * by using targetIntent. + * + *

If this method returns false the app can trivially call + * {@link #navigateUpTo(Activity, Intent)} using the same parameters to correctly perform + * up navigation. If this method returns true, the app should synthesize a new task stack + * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + * @param targetIntent An intent representing the target destination for up navigation + * @return true if navigating up should recreate a new task stack, false if the same task + * should be used for the destination + */ + public static boolean shouldUpRecreateTask(Activity sourceActivity, Intent targetIntent) { + return IMPL.shouldUpRecreateTask(sourceActivity, targetIntent); + } + + /** + * Convenience method that is equivalent to calling + * {@link #navigateUpTo(Activity, Intent) navigateUpTo}(sourceActivity, + * {@link #getParentActivityIntent(Activity) getParentActivityIntent} (sourceActivity)). + * sourceActivity will be finished by this call. + * + *

Note: This method should only be used when sourceActivity and the corresponding + * parent are within the same task. If up navigation should cross tasks in some cases, see + * {@link #shouldUpRecreateTask(Activity, Intent)}.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + */ + public static void navigateUpFromSameTask(Activity sourceActivity) { + Intent upIntent = getParentActivityIntent(sourceActivity); + + if (upIntent == null) { + throw new IllegalArgumentException("Activity " + + sourceActivity.getClass().getSimpleName() + + " does not have a parent activity name specified." + + " (Did you forget to add the android.support.PARENT_ACTIVITY " + + " element in your manifest?)"); + } + + navigateUpTo(sourceActivity, upIntent); + } + + /** + * Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity + * in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set + * by this method, along with any others required for proper up navigation as outlined + * in the Android Design Guide. + * + *

This method should be used when performing up navigation from within the same task + * as the destination. If up navigation should cross tasks in some cases, see + * {@link #shouldUpRecreateTask(Activity, Intent)}.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + * @param upIntent An intent representing the target destination for up navigation + */ + public static void navigateUpTo(Activity sourceActivity, Intent upIntent) { + IMPL.navigateUpTo(sourceActivity, upIntent); + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivity's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. If the device is running + * Jellybean or newer, the android:parentActivityName attribute will be preferred + * if it is present. + * + * @param sourceActivity Activity to fetch a parent intent for + * @return a new Intent targeting the defined parent activity of sourceActivity + */ + public static Intent getParentActivityIntent(Activity sourceActivity) { + return IMPL.getParentActivityIntent(sourceActivity); + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. + * + * @param context Context for looking up the activity component for sourceActivityClass + * @param sourceActivityClass {@link java.lang.Class} object for an Activity class + * @return a new Intent targeting the defined parent activity of sourceActivity + * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid + */ + public static Intent getParentActivityIntent(Context context, Class sourceActivityClass) + throws NameNotFoundException { + String parentActivity = getParentActivityName(context, + new ComponentName(context, sourceActivityClass)); + if (parentActivity == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName(context, parentActivity); + final String grandparent = getParentActivityName(context, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. + * + * @param context Context for looking up the activity component for the source activity + * @param componentName ComponentName for the source Activity + * @return a new Intent targeting the defined parent activity of sourceActivity + * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid + */ + public static Intent getParentActivityIntent(Context context, ComponentName componentName) + throws NameNotFoundException { + String parentActivity = getParentActivityName(context, componentName); + if (parentActivity == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName( + componentName.getPackageName(), parentActivity); + final String grandparent = getParentActivityName(context, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } + + /** + * Return the fully qualified class name of sourceActivity's parent activity as specified by + * a {@link #PARENT_ACTIVITY} <meta-data> element within the activity element in + * the application's manifest. + * + * @param sourceActivity Activity to fetch a parent class name for + * @return The fully qualified class name of sourceActivity's parent activity or null if + * it was not specified + */ + @Nullable + public static String getParentActivityName(Activity sourceActivity) { + try { + return getParentActivityName(sourceActivity, sourceActivity.getComponentName()); + } catch (NameNotFoundException e) { + // Component name of supplied activity does not exist...? + throw new IllegalArgumentException(e); + } + } + /** + * Return the fully qualified class name of a source activity's parent activity as specified by + * a {@link #PARENT_ACTIVITY} <meta-data> element within the activity element in + * the application's manifest. The source activity is provided by componentName. + * + * @param context Context for looking up the activity component for the source activity + * @param componentName ComponentName for the source Activity + * @return The fully qualified class name of sourceActivity's parent activity or null if + * it was not specified + */ + @Nullable + public static String getParentActivityName(Context context, ComponentName componentName) + throws NameNotFoundException { + PackageManager pm = context.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA); + String parentActivity = IMPL.getParentActivityName(context, info); + return parentActivity; + } + + /** No instances! */ + private NavUtils() { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java new file mode 100644 index 00000000000..a9a76c2263c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; + +class NavUtilsJB { + public static Intent getParentActivityIntent(Activity activity) { + return activity.getParentActivityIntent(); + } + + public static boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + return activity.shouldUpRecreateTask(targetIntent); + } + + public static void navigateUpTo(Activity activity, Intent upIntent) { + activity.navigateUpTo(upIntent); + } + + public static String getParentActivityName(ActivityInfo info) { + return info.parentActivityName; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java b/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java new file mode 100644 index 00000000000..d0ebb96e8e0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content.Context; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Pre-Honeycomb versions of the platform don't have {@link View#setSaveFromParentEnabled(boolean)}, + * so instead we insert this between the view and its parent. + */ +class NoSaveStateFrameLayout extends FrameLayout { + static ViewGroup wrap(View child) { + NoSaveStateFrameLayout wrapper = new NoSaveStateFrameLayout(child.getContext()); + ViewGroup.LayoutParams childParams = child.getLayoutParams(); + if (childParams != null) { + wrapper.setLayoutParams(childParams); + } + NoSaveStateFrameLayout.LayoutParams lp = new NoSaveStateFrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT); + child.setLayoutParams(lp); + wrapper.addView(child); + return wrapper; + } + + public NoSaveStateFrameLayout(Context context) { + super(context); + } + + /** + * Override to prevent freezing of any child views. + */ + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any child views. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + dispatchThawSelfOnly(container); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java new file mode 100644 index 00000000000..8e8d8ce4b74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +package android.support.v4.app; + +/** + * Interface implemented by notification compat builders that support adding actions. + */ +interface NotificationBuilderWithActions { + public void addAction(NotificationCompatBase.Action action); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java new file mode 100644 index 00000000000..f1b2d82ba67 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; + +/** + * Interface implemented by notification compat builders that support + * an accessor for {@link Notification.Builder}. {@link Notification.Builder} + * was introduced in HoneyComb. + * + * @hide + */ +public interface NotificationBuilderWithBuilderAccessor { + public Notification.Builder getBuilder(); + public Notification build(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java new file mode 100644 index 00000000000..99c269ea5bb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java @@ -0,0 +1,3360 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.GravityCompat; +import android.view.Gravity; +import android.widget.RemoteViews; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing features in {@link android.app.Notification} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class NotificationCompat { + + /** + * Use all default values (where applicable). + */ + public static final int DEFAULT_ALL = ~0; + + /** + * Use the default notification sound. This will ignore any sound set using + * {@link Builder#setSound} + * + *

+ * A notification that is noisy is more likely to be presented as a heads-up notification, + * on some platforms. + *

+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_SOUND = 1; + + /** + * Use the default notification vibrate. This will ignore any vibrate set using + * {@link Builder#setVibrate}. Using phone vibration requires the + * {@link android.Manifest.permission#VIBRATE VIBRATE} permission. + * + *

+ * A notification that vibrates is more likely to be presented as a heads-up notification, + * on some platforms. + *

+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_VIBRATE = 2; + + /** + * Use the default notification lights. This will ignore the + * {@link #FLAG_SHOW_LIGHTS} bit, and values set with {@link Builder#setLights}. + * + * @see Builder#setDefaults + */ + public static final int DEFAULT_LIGHTS = 4; + + /** + * Use this constant as the value for audioStreamType to request that + * the default stream type for notifications be used. Currently the + * default stream type is {@link AudioManager#STREAM_NOTIFICATION}. + */ + public static final int STREAM_DEFAULT = -1; + + /** + * Bit set in the Notification flags field when LEDs should be turned on + * for this notification. + */ + public static final int FLAG_SHOW_LIGHTS = 0x00000001; + + /** + * Bit set in the Notification flags field if this notification is in + * reference to something that is ongoing, like a phone call. It should + * not be set if this notification is in reference to something that + * happened at a particular point in time, like a missed phone call. + */ + public static final int FLAG_ONGOING_EVENT = 0x00000002; + + /** + * Bit set in the Notification flags field if + * the audio will be repeated until the notification is + * cancelled or the notification window is opened. + */ + public static final int FLAG_INSISTENT = 0x00000004; + + /** + * Bit set in the Notification flags field if the notification's sound, + * vibrate and ticker should only be played if the notification is not already showing. + */ + public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008; + + /** + * Bit set in the Notification flags field if the notification should be canceled when + * it is clicked by the user. + */ + public static final int FLAG_AUTO_CANCEL = 0x00000010; + + /** + * Bit set in the Notification flags field if the notification should not be canceled + * when the user clicks the Clear all button. + */ + public static final int FLAG_NO_CLEAR = 0x00000020; + + /** + * Bit set in the Notification flags field if this notification represents a currently + * running service. This will normally be set for you by + * {@link android.app.Service#startForeground}. + */ + public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; + + /** + * Obsolete flag indicating high-priority notifications; use the priority field instead. + * + * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value. + */ + public static final int FLAG_HIGH_PRIORITY = 0x00000080; + + /** + * Bit set in the Notification flags field if this notification is relevant to the current + * device only and it is not recommended that it bridge to other devices. + */ + public static final int FLAG_LOCAL_ONLY = 0x00000100; + + /** + * Bit set in the Notification flags field if this notification is the group summary for a + * group of notifications. Grouped notifications may display in a cluster or stack on devices + * which support such rendering. Requires a group key also be set using + * {@link Builder#setGroup}. + */ + public static final int FLAG_GROUP_SUMMARY = 0x00000200; + + /** + * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}. + * If your application does not prioritize its own notifications, + * use this value for all notifications. + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Lower notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for items that are less important. The UI may choose to show + * these items smaller, or at a different position in the list, + * compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_LOW = -1; + + /** + * Lowest notification priority for {@link NotificationCompat.Builder#setPriority(int)}; + * these items might not be shown to the user except under + * special circumstances, such as detailed notification logs. + */ + public static final int PRIORITY_MIN = -2; + + /** + * Higher notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for more important notifications or alerts. The UI may choose + * to show these items larger, or at a different position in + * notification lists, compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_HIGH = 1; + + /** + * Highest notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for your application's most important items that require the user's + * prompt attention or input. + */ + public static final int PRIORITY_MAX = 2; + + /** + * Notification extras key: this is the title of the notification, + * as supplied to {@link Builder#setContentTitle(CharSequence)}. + */ + public static final String EXTRA_TITLE = "android.title"; + + /** + * Notification extras key: this is the title of the notification when shown in expanded form, + * e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}. + */ + public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big"; + + /** + * Notification extras key: this is the main text payload, as supplied to + * {@link Builder#setContentText(CharSequence)}. + */ + public static final String EXTRA_TEXT = "android.text"; + + /** + * Notification extras key: this is a third line of text, as supplied to + * {@link Builder#setSubText(CharSequence)}. + */ + public static final String EXTRA_SUB_TEXT = "android.subText"; + + /** + * Notification extras key: this is a small piece of additional text as supplied to + * {@link Builder#setContentInfo(CharSequence)}. + */ + public static final String EXTRA_INFO_TEXT = "android.infoText"; + + /** + * Notification extras key: this is a line of summary information intended to be shown + * alongside expanded notifications, as supplied to (e.g.) + * {@link BigTextStyle#setSummaryText(CharSequence)}. + */ + public static final String EXTRA_SUMMARY_TEXT = "android.summaryText"; + + /** + * Notification extras key: this is the longer text shown in the big form of a + * {@link BigTextStyle} notification, as supplied to + * {@link BigTextStyle#bigText(CharSequence)}. + */ + public static final String EXTRA_BIG_TEXT = "android.bigText"; + + /** + * Notification extras key: this is the resource ID of the notification's main small icon, as + * supplied to {@link Builder#setSmallIcon(int)}. + */ + public static final String EXTRA_SMALL_ICON = "android.icon"; + + /** + * Notification extras key: this is a bitmap to be used instead of the small icon when showing the + * notification payload, as + * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}. + */ + public static final String EXTRA_LARGE_ICON = "android.largeIcon"; + + /** + * Notification extras key: this is a bitmap to be used instead of the one from + * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is + * shown in its expanded form, as supplied to + * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}. + */ + public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big"; + + /** + * Notification extras key: this is the progress value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS = "android.progress"; + + /** + * Notification extras key: this is the maximum value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS_MAX = "android.progressMax"; + + /** + * Notification extras key: whether the progress bar is indeterminate, supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate"; + + /** + * Notification extras key: whether the when field set using {@link Builder#setWhen} should + * be shown as a count-up timer (specifically a {@link android.widget.Chronometer}) instead + * of a timestamp, as supplied to {@link Builder#setUsesChronometer(boolean)}. + */ + public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer"; + + /** + * Notification extras key: whether the when field set using {@link Builder#setWhen} should + * be shown, as supplied to {@link Builder#setShowWhen(boolean)}. + */ + public static final String EXTRA_SHOW_WHEN = "android.showWhen"; + + /** + * Notification extras key: this is a bitmap to be shown in {@link BigPictureStyle} expanded + * notifications, supplied to {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}. + */ + public static final String EXTRA_PICTURE = "android.picture"; + + /** + * Notification extras key: An array of CharSequences to show in {@link InboxStyle} expanded + * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}. + */ + public static final String EXTRA_TEXT_LINES = "android.textLines"; + + /** + * Notification extras key: A string representing the name of the specific + * {@link android.app.Notification.Style} used to create this notification. + */ + public static final String EXTRA_TEMPLATE = "android.template"; + + /** + * Notification extras key: A String array containing the people that this + * notification relates to, each of which was supplied to + * {@link Builder#addPerson(String)}. + */ + public static final String EXTRA_PEOPLE = "android.people"; + + /** + * Notification extras key: A + * {@link android.content.ContentUris content URI} pointing to an image that can be displayed + * in the background when the notification is selected. The URI must point to an image stream + * suitable for passing into + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider + * URI used for this purpose must require no permissions to read the image data. + */ + public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; + + /** + * Notification key: A + * {@link android.media.session.MediaSession.Token} associated with a + * {@link android.app.Notification.MediaStyle} notification. + */ + public static final String EXTRA_MEDIA_SESSION = "android.mediaSession"; + + /** + * Notification extras key: the indices of actions to be shown in the compact view, + * as supplied to (e.g.) {@link Notification.MediaStyle#setShowActionsInCompactView(int...)}. + */ + public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions"; + + /** + * Value of {@link Notification#color} equal to 0 (also known as + * {@link android.graphics.Color#TRANSPARENT Color.TRANSPARENT}), + * telling the system not to decorate this notification with any special color but instead use + * default colors when presenting this notification. + */ + public static final int COLOR_DEFAULT = Color.TRANSPARENT; + + /** + * Notification visibility: Show this notification in its entirety on all lockscreens. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_PUBLIC = 1; + + /** + * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or + * private information on secure lockscreens. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_PRIVATE = 0; + + /** + * Notification visibility: Do not reveal any part of this notification on a secure lockscreen. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_SECRET = -1; + + /** + * Notification category: incoming call (voice or video) or similar synchronous communication request. + */ + public static final String CATEGORY_CALL = NotificationCompatApi21.CATEGORY_CALL; + + /** + * Notification category: incoming direct message (SMS, instant message, etc.). + */ + public static final String CATEGORY_MESSAGE = NotificationCompatApi21.CATEGORY_MESSAGE; + + /** + * Notification category: asynchronous bulk message (email). + */ + public static final String CATEGORY_EMAIL = NotificationCompatApi21.CATEGORY_EMAIL; + + /** + * Notification category: calendar event. + */ + public static final String CATEGORY_EVENT = NotificationCompatApi21.CATEGORY_EVENT; + + /** + * Notification category: promotion or advertisement. + */ + public static final String CATEGORY_PROMO = NotificationCompatApi21.CATEGORY_PROMO; + + /** + * Notification category: alarm or timer. + */ + public static final String CATEGORY_ALARM = NotificationCompatApi21.CATEGORY_ALARM; + + /** + * Notification category: progress of a long-running background operation. + */ + public static final String CATEGORY_PROGRESS = NotificationCompatApi21.CATEGORY_PROGRESS; + + /** + * Notification category: social network or sharing update. + */ + public static final String CATEGORY_SOCIAL = NotificationCompatApi21.CATEGORY_SOCIAL; + + /** + * Notification category: error in background operation or authentication status. + */ + public static final String CATEGORY_ERROR = NotificationCompatApi21.CATEGORY_ERROR; + + /** + * Notification category: media transport control for playback. + */ + public static final String CATEGORY_TRANSPORT = NotificationCompatApi21.CATEGORY_TRANSPORT; + + /** + * Notification category: system or device status update. Reserved for system use. + */ + public static final String CATEGORY_SYSTEM = NotificationCompatApi21.CATEGORY_SYSTEM; + + /** + * Notification category: indication of running background service. + */ + public static final String CATEGORY_SERVICE = NotificationCompatApi21.CATEGORY_SERVICE; + + /** + * Notification category: a specific, timely recommendation for a single thing. + * For example, a news app might want to recommend a news story it believes the user will + * want to read next. + */ + public static final String CATEGORY_RECOMMENDATION = + NotificationCompatApi21.CATEGORY_RECOMMENDATION; + + /** + * Notification category: ongoing information about device or contextual status. + */ + public static final String CATEGORY_STATUS = NotificationCompatApi21.CATEGORY_STATUS; + + private static final NotificationCompatImpl IMPL; + + interface NotificationCompatImpl { + public Notification build(Builder b, BuilderExtender extender); + public Bundle getExtras(Notification n); + public int getActionCount(Notification n); + public Action getAction(Notification n, int actionIndex); + public Action[] getActionsFromParcelableArrayList(ArrayList parcelables); + public ArrayList getParcelableArrayListForActions(Action[] actions); + public String getCategory(Notification n); + public boolean getLocalOnly(Notification n); + public String getGroup(Notification n); + public boolean isGroupSummary(Notification n); + public String getSortKey(Notification n); + Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc); + NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory); + } + + /** + * Interface for appcompat to extend v4 builder with media style. + * + * @hide + */ + protected static class BuilderExtender { + public Notification build(Builder b, NotificationBuilderWithBuilderAccessor builder) { + return builder.build(); + } + } + + static class NotificationCompatImplBase implements NotificationCompatImpl { + @Override + public Notification build(Builder b, BuilderExtender extender) { + Notification result = b.mNotification; + result.setLatestEventInfo(b.mContext, b.mContentTitle, + b.mContentText, b.mContentIntent); + // translate high priority requests into legacy flag + if (b.mPriority > PRIORITY_DEFAULT) { + result.flags |= FLAG_HIGH_PRIORITY; + } + return result; + } + + @Override + public Bundle getExtras(Notification n) { + return null; + } + + @Override + public int getActionCount(Notification n) { + return 0; + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return null; + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return null; + } + + @Override + public ArrayList getParcelableArrayListForActions(Action[] actions) { + return null; + } + + @Override + public String getCategory(Notification n) { + return null; + } + + @Override + public boolean getLocalOnly(Notification n) { + return false; + } + + @Override + public String getGroup(Notification n) { + return null; + } + + @Override + public boolean isGroupSummary(Notification n) { + return false; + } + + @Override + public String getSortKey(Notification n) { + return null; + } + + @Override + public Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + return null; + } + + @Override + public NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return null; + } + } + + static class NotificationCompatImplGingerbread extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + Notification result = b.mNotification; + result.setLatestEventInfo(b.mContext, b.mContentTitle, + b.mContentText, b.mContentIntent); + result = NotificationCompatGingerbread.add(result, b.mContext, + b.mContentTitle, b.mContentText, b.mContentIntent, b.mFullScreenIntent); + // translate high priority requests into legacy flag + if (b.mPriority > PRIORITY_DEFAULT) { + result.flags |= FLAG_HIGH_PRIORITY; + } + return result; + } + } + + static class NotificationCompatImplHoneycomb extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + return NotificationCompatHoneycomb.add(b.mContext, b.mNotification, + b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView, + b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon); + } + } + + static class NotificationCompatImplIceCreamSandwich extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatIceCreamSandwich.Builder builder = + new NotificationCompatIceCreamSandwich.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate); + return extender.build(b, builder); + } + } + + static class NotificationCompatImplJellybean extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatJellybean.Builder builder = new NotificationCompatJellybean.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Bundle getExtras(Notification n) { + return NotificationCompatJellybean.getExtras(n); + } + + @Override + public int getActionCount(Notification n) { + return NotificationCompatJellybean.getActionCount(n); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatJellybean.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return (Action[]) NotificationCompatJellybean.getActionsFromParcelableArrayList( + parcelables, Action.FACTORY, RemoteInput.FACTORY); + } + + @Override + public ArrayList getParcelableArrayListForActions( + Action[] actions) { + return NotificationCompatJellybean.getParcelableArrayListForActions(actions); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatJellybean.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatJellybean.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatJellybean.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatJellybean.getSortKey(n); + } + } + + static class NotificationCompatImplKitKat extends NotificationCompatImplJellybean { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, + b.mPeople, b.mExtras, b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Bundle getExtras(Notification n) { + return NotificationCompatKitKat.getExtras(n); + } + + @Override + public int getActionCount(Notification n) { + return NotificationCompatKitKat.getActionCount(n); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatKitKat.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatKitKat.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatKitKat.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatKitKat.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatKitKat.getSortKey(n); + } + } + + static class NotificationCompatImplApi20 extends NotificationCompatImplKitKat { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatApi20.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return (Action[]) NotificationCompatApi20.getActionsFromParcelableArrayList( + parcelables, Action.FACTORY, RemoteInput.FACTORY); + } + + @Override + public ArrayList getParcelableArrayListForActions( + Action[] actions) { + return NotificationCompatApi20.getParcelableArrayListForActions(actions); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatApi20.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatApi20.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatApi20.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatApi20.getSortKey(n); + } + } + + static class NotificationCompatImplApi21 extends NotificationCompatImplApi20 { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatApi21.Builder builder = new NotificationCompatApi21.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory, + b.mPeople, b.mExtras, b.mColor, b.mVisibility, b.mPublicVersion, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public String getCategory(Notification notif) { + return NotificationCompatApi21.getCategory(notif); + } + + @Override + public Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + return NotificationCompatApi21.getBundleForUnreadConversation(uc); + } + + @Override + public NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return NotificationCompatApi21.getUnreadConversationFromBundle( + b, factory, remoteInputFactory); + } + } + + private static void addActionsToBuilder(NotificationBuilderWithActions builder, + ArrayList actions) { + for (Action action : actions) { + builder.addAction(action); + } + } + + private static void addStyleToBuilderJellybean(NotificationBuilderWithBuilderAccessor builder, + Style style) { + if (style != null) { + if (style instanceof BigTextStyle) { + BigTextStyle bigTextStyle = (BigTextStyle) style; + NotificationCompatJellybean.addBigTextStyle(builder, + bigTextStyle.mBigContentTitle, + bigTextStyle.mSummaryTextSet, + bigTextStyle.mSummaryText, + bigTextStyle.mBigText); + } else if (style instanceof InboxStyle) { + InboxStyle inboxStyle = (InboxStyle) style; + NotificationCompatJellybean.addInboxStyle(builder, + inboxStyle.mBigContentTitle, + inboxStyle.mSummaryTextSet, + inboxStyle.mSummaryText, + inboxStyle.mTexts); + } else if (style instanceof BigPictureStyle) { + BigPictureStyle bigPictureStyle = (BigPictureStyle) style; + NotificationCompatJellybean.addBigPictureStyle(builder, + bigPictureStyle.mBigContentTitle, + bigPictureStyle.mSummaryTextSet, + bigPictureStyle.mSummaryText, + bigPictureStyle.mPicture, + bigPictureStyle.mBigLargeIcon, + bigPictureStyle.mBigLargeIconSet); + } + } + } + + static { + if (Build.VERSION.SDK_INT >= 21) { + IMPL = new NotificationCompatImplApi21(); + } else if (Build.VERSION.SDK_INT >= 20) { + IMPL = new NotificationCompatImplApi20(); + } else if (Build.VERSION.SDK_INT >= 19) { + IMPL = new NotificationCompatImplKitKat(); + } else if (Build.VERSION.SDK_INT >= 16) { + IMPL = new NotificationCompatImplJellybean(); + } else if (Build.VERSION.SDK_INT >= 14) { + IMPL = new NotificationCompatImplIceCreamSandwich(); + } else if (Build.VERSION.SDK_INT >= 11) { + IMPL = new NotificationCompatImplHoneycomb(); + } else if (Build.VERSION.SDK_INT >= 9) { + IMPL = new NotificationCompatImplGingerbread(); + } else { + IMPL = new NotificationCompatImplBase(); + } + } + + /** + * Builder class for {@link NotificationCompat} objects. Allows easier control over + * all the flags, as well as help constructing the typical notification layouts. + *

+ * On platform versions that don't offer expanded notifications, methods that depend on + * expanded notifications have no effect. + *

+ *

+ * For example, action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. + *

+ * For this reason, you should always ensure that UI controls in a notification are also + * available in an {@link android.app.Activity} in your app, and you should always start that + * {@link android.app.Activity} when users click the notification. To do this, use the + * {@link NotificationCompat.Builder#setContentIntent setContentIntent()} + * method. + *

+ * + */ + public static class Builder { + /** + * Maximum length of CharSequences accepted by Builder and friends. + * + *

+ * Avoids spamming the system with overly large strings such as full e-mails. + */ + private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; + + // All these variables are declared public/hidden so they can be accessed by a builder + // extender. + + /** @hide */ + public Context mContext; + + /** @hide */ + public CharSequence mContentTitle; + /** @hide */ + public CharSequence mContentText; + PendingIntent mContentIntent; + PendingIntent mFullScreenIntent; + RemoteViews mTickerView; + /** @hide */ + public Bitmap mLargeIcon; + /** @hide */ + public CharSequence mContentInfo; + /** @hide */ + public int mNumber; + int mPriority; + boolean mShowWhen = true; + /** @hide */ + public boolean mUseChronometer; + /** @hide */ + public Style mStyle; + /** @hide */ + public CharSequence mSubText; + int mProgressMax; + int mProgress; + boolean mProgressIndeterminate; + String mGroupKey; + boolean mGroupSummary; + String mSortKey; + /** @hide */ + public ArrayList mActions = new ArrayList(); + boolean mLocalOnly = false; + String mCategory; + Bundle mExtras; + int mColor = COLOR_DEFAULT; + int mVisibility = VISIBILITY_PRIVATE; + Notification mPublicVersion; + + /** @hide */ + public Notification mNotification = new Notification(); + public ArrayList mPeople; + + /** + * Constructor. + * + * Automatically sets the when field to {@link System#currentTimeMillis() + * System.currentTimeMillis()} and the audio stream to the + * {@link Notification#STREAM_DEFAULT}. + * + * @param context A {@link Context} that will be used to construct the + * RemoteViews. The Context will not be held past the lifetime of this + * Builder object. + */ + public Builder(Context context) { + mContext = context; + + // Set defaults to match the defaults of a Notification + mNotification.when = System.currentTimeMillis(); + mNotification.audioStreamType = Notification.STREAM_DEFAULT; + mPriority = PRIORITY_DEFAULT; + mPeople = new ArrayList(); + } + + /** + * Set the time that the event occurred. Notifications in the panel are + * sorted by this time. + */ + public Builder setWhen(long when) { + mNotification.when = when; + return this; + } + + /** + * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown + * in the content view. + */ + public Builder setShowWhen(boolean show) { + mShowWhen = show; + return this; + } + + /** + * Show the {@link Notification#when} field as a stopwatch. + * + * Instead of presenting when as a timestamp, the notification will show an + * automatically updating display of the minutes and seconds since when. + * + * Useful when showing an elapsed time (like an ongoing phone call). + * + * @see android.widget.Chronometer + * @see Notification#when + */ + public Builder setUsesChronometer(boolean b) { + mUseChronometer = b; + return this; + } + + /** + * Set the small icon to use in the notification layouts. Different classes of devices + * may return different sizes. See the UX guidelines for more information on how to + * design these icons. + * + * @param icon A resource ID in the application's package of the drawble to use. + */ + public Builder setSmallIcon(int icon) { + mNotification.icon = icon; + return this; + } + + /** + * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional + * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable + * LevelListDrawable}. + * + * @param icon A resource ID in the application's package of the drawble to use. + * @param level The level to use for the icon. + * + * @see android.graphics.drawable.LevelListDrawable + */ + public Builder setSmallIcon(int icon, int level) { + mNotification.icon = icon; + mNotification.iconLevel = level; + return this; + } + + /** + * Set the title (first row) of the notification, in a standard notification. + */ + public Builder setContentTitle(CharSequence title) { + mContentTitle = limitCharSequenceLength(title); + return this; + } + + /** + * Set the text (second row) of the notification, in a standard notification. + */ + public Builder setContentText(CharSequence text) { + mContentText = limitCharSequenceLength(text); + return this; + } + + /** + * Set the third line of text in the platform notification template. + * Don't use if you're also using {@link #setProgress(int, int, boolean)}; + * they occupy the same location in the standard template. + *
+ * If the platform does not provide large-format notifications, this method has no effect. + * The third line of text only appears in expanded view. + *
+ */ + public Builder setSubText(CharSequence text) { + mSubText = limitCharSequenceLength(text); + return this; + } + + /** + * Set the large number at the right-hand side of the notification. This is + * equivalent to setContentInfo, although it might show the number in a different + * font size for readability. + */ + public Builder setNumber(int number) { + mNumber = number; + return this; + } + + /** + * Set the large text at the right-hand side of the notification. + */ + public Builder setContentInfo(CharSequence info) { + mContentInfo = limitCharSequenceLength(info); + return this; + } + + /** + * Set the progress this notification represents, which may be + * represented as a {@link android.widget.ProgressBar}. + */ + public Builder setProgress(int max, int progress, boolean indeterminate) { + mProgressMax = max; + mProgress = progress; + mProgressIndeterminate = indeterminate; + return this; + } + + /** + * Supply a custom RemoteViews to use instead of the standard one. + */ + public Builder setContent(RemoteViews views) { + mNotification.contentView = views; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is clicked. + * If you do not supply an intent, you can now add PendingIntents to individual + * views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent + * RemoteViews.setOnClickPendingIntent(int,PendingIntent)}. Be sure to + * read {@link Notification#contentIntent Notification.contentIntent} for + * how to correctly use this. + */ + public Builder setContentIntent(PendingIntent intent) { + mContentIntent = intent; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is cleared by the user + * directly from the notification panel. For example, this intent is sent when the user + * clicks the "Clear all" button, or the individual "X" buttons on notifications. This + * intent is not sent when the application calls + * {@link android.app.NotificationManager#cancel NotificationManager.cancel(int)}. + */ + public Builder setDeleteIntent(PendingIntent intent) { + mNotification.deleteIntent = intent; + return this; + } + + /** + * An intent to launch instead of posting the notification to the status bar. + * Only for use with extremely high-priority notifications demanding the user's + * immediate attention, such as an incoming phone call or + * alarm clock that the user has explicitly set to a particular time. + * If this facility is used for something else, please give the user an option + * to turn it off and use a normal notification, as this can be extremely + * disruptive. + * + *

+ * On some platforms, the system UI may choose to display a heads-up notification, + * instead of launching this intent, while the user is using the device. + *

+ * + * @param intent The pending intent to launch. + * @param highPriority Passing true will cause this notification to be sent + * even if other notifications are suppressed. + */ + public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) { + mFullScreenIntent = intent; + setFlag(FLAG_HIGH_PRIORITY, highPriority); + return this; + } + + /** + * Set the text that is displayed in the status bar when the notification first + * arrives. + */ + public Builder setTicker(CharSequence tickerText) { + mNotification.tickerText = limitCharSequenceLength(tickerText); + return this; + } + + /** + * Set the text that is displayed in the status bar when the notification first + * arrives, and also a RemoteViews object that may be displayed instead on some + * devices. + */ + public Builder setTicker(CharSequence tickerText, RemoteViews views) { + mNotification.tickerText = limitCharSequenceLength(tickerText); + mTickerView = views; + return this; + } + + /** + * Set the large icon that is shown in the ticker and notification. + */ + public Builder setLargeIcon(Bitmap icon) { + mLargeIcon = icon; + return this; + } + + /** + * Set the sound to play. It will play on the default stream. + * + *

+ * On some platforms, a notification that is noisy is more likely to be presented + * as a heads-up notification. + *

+ */ + public Builder setSound(Uri sound) { + mNotification.sound = sound; + mNotification.audioStreamType = Notification.STREAM_DEFAULT; + return this; + } + + /** + * Set the sound to play. It will play on the stream you supply. + * + *

+ * On some platforms, a notification that is noisy is more likely to be presented + * as a heads-up notification. + *

+ * + * @see Notification#STREAM_DEFAULT + * @see AudioManager for the STREAM_ constants. + */ + public Builder setSound(Uri sound, int streamType) { + mNotification.sound = sound; + mNotification.audioStreamType = streamType; + return this; + } + + /** + * Set the vibration pattern to use. + * + *

+ * On some platforms, a notification that vibrates is more likely to be presented + * as a heads-up notification. + *

+ * + * @see android.os.Vibrator for a discussion of the pattern + * parameter. + */ + public Builder setVibrate(long[] pattern) { + mNotification.vibrate = pattern; + return this; + } + + /** + * Set the argb value that you would like the LED on the device to blnk, as well as the + * rate. The rate is specified in terms of the number of milliseconds to be on + * and then the number of milliseconds to be off. + */ + public Builder setLights(int argb, int onMs, int offMs) { + mNotification.ledARGB = argb; + mNotification.ledOnMS = onMs; + mNotification.ledOffMS = offMs; + boolean showLights = mNotification.ledOnMS != 0 && mNotification.ledOffMS != 0; + mNotification.flags = (mNotification.flags & ~Notification.FLAG_SHOW_LIGHTS) | + (showLights ? Notification.FLAG_SHOW_LIGHTS : 0); + return this; + } + + /** + * Set whether this is an ongoing notification. + * + *

Ongoing notifications differ from regular notifications in the following ways: + *

    + *
  • Ongoing notifications are sorted above the regular notifications in the + * notification panel.
  • + *
  • Ongoing notifications do not have an 'X' close button, and are not affected + * by the "Clear all" button. + *
+ */ + public Builder setOngoing(boolean ongoing) { + setFlag(Notification.FLAG_ONGOING_EVENT, ongoing); + return this; + } + + /** + * Set this flag if you would only like the sound, vibrate + * and ticker to be played if the notification is not already showing. + */ + public Builder setOnlyAlertOnce(boolean onlyAlertOnce) { + setFlag(Notification.FLAG_ONLY_ALERT_ONCE, onlyAlertOnce); + return this; + } + + /** + * Setting this flag will make it so the notification is automatically + * canceled when the user clicks it in the panel. The PendingIntent + * set with {@link #setDeleteIntent} will be broadcast when the notification + * is canceled. + */ + public Builder setAutoCancel(boolean autoCancel) { + setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel); + return this; + } + + /** + * Set whether or not this notification is only relevant to the current device. + * + *

Some notifications can be bridged to other devices for remote display. + * This hint can be set to recommend this notification not be bridged. + */ + public Builder setLocalOnly(boolean b) { + mLocalOnly = b; + return this; + } + + /** + * Set the notification category. + * + *

Must be one of the predefined notification categories (see the CATEGORY_* + * constants in {@link Notification}) that best describes this notification. + * May be used by the system for ranking and filtering. + */ + public Builder setCategory(String category) { + mCategory = category; + return this; + } + + /** + * Set the default notification options that will be used. + *

+ * The value should be one or more of the following fields combined with + * bitwise-or: + * {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE}, + * {@link Notification#DEFAULT_LIGHTS}. + *

+ * For all default values, use {@link Notification#DEFAULT_ALL}. + */ + public Builder setDefaults(int defaults) { + mNotification.defaults = defaults; + if ((defaults & Notification.DEFAULT_LIGHTS) != 0) { + mNotification.flags |= Notification.FLAG_SHOW_LIGHTS; + } + return this; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mNotification.flags |= mask; + } else { + mNotification.flags &= ~mask; + } + } + + /** + * Set the relative priority for this notification. + * + * Priority is an indication of how much of the user's + * valuable attention should be consumed by this + * notification. Low-priority notifications may be hidden from + * the user in certain situations, while the user might be + * interrupted for a higher-priority notification. + * The system sets a notification's priority based on various factors including the + * setPriority value. The effect may differ slightly on different platforms. + * + * @param pri Relative priority for this notification. Must be one of + * the priority constants defined by {@link NotificationCompat}. + * Acceptable values range from {@link + * NotificationCompat#PRIORITY_MIN} (-2) to {@link + * NotificationCompat#PRIORITY_MAX} (2). + */ + public Builder setPriority(int pri) { + mPriority = pri; + return this; + } + + /** + * Add a person that is relevant to this notification. + * + *

+ * Depending on user preferences, this annotation may allow the notification to pass + * through interruption filters, and to appear more prominently in the user interface. + *

+ * + *

+ * The person should be specified by the {@code String} representation of a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + *

+ * + *

The system will also attempt to resolve {@code mailto:} and {@code tel:} schema + * URIs. The path part of these URIs must exist in the contacts database, in the + * appropriate column, or the reference will be discarded as invalid. Telephone schema + * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. + *

+ * + * @param uri A URI for the person. + * @see Notification#EXTRA_PEOPLE + */ + public Builder addPerson(String uri) { + mPeople.add(uri); + return this; + } + + /** + * Set this notification to be part of a group of notifications sharing the same key. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. + * + *

To make this notification the summary for its group, also call + * {@link #setGroupSummary}. A sort order can be specified for group members by using + * {@link #setSortKey}. + * @param groupKey The group key of the group. + * @return this object for method chaining + */ + public Builder setGroup(String groupKey) { + mGroupKey = groupKey; + return this; + } + + /** + * Set this notification to be the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link #setGroup}. + * @param isGroupSummary Whether this notification should be a group summary. + * @return this object for method chaining + */ + public Builder setGroupSummary(boolean isGroupSummary) { + mGroupSummary = isGroupSummary; + return this; + } + + /** + * Set a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + *

This sort key can also be used to order members of a notification group. See + * {@link Builder#setGroup}. + * + * @see String#compareTo(String) + */ + public Builder setSortKey(String sortKey) { + mSortKey = sortKey; + return this; + } + + /** + * Merge additional metadata into this notification. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see Notification#extras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + if (mExtras == null) { + mExtras = new Bundle(extras); + } else { + mExtras.putAll(extras); + } + } + return this; + } + + /** + * Set metadata for this notification. + * + *

A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's + * current contents are copied into the Notification each time {@link #build()} is + * called. + * + *

Replaces any existing extras values with those from the provided Bundle. + * Use {@link #addExtras} to merge in metadata instead. + * + * @see Notification#extras + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Get the current metadata Bundle used by this notification Builder. + * + *

The returned Bundle is shared with this Builder. + * + *

The current contents of this Bundle are copied into the Notification each time + * {@link #build()} is called. + * + * @see Notification#extras + */ + public Bundle getExtras() { + if (mExtras == null) { + mExtras = new Bundle(); + } + return mExtras; + } + + /** + * Add an action to this notification. Actions are typically displayed by + * the system as a button adjacent to the notification content. + *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. To ensure that an action button's functionality is always available, first + * implement the functionality in the {@link android.app.Activity} that starts when a user + * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then + * enhance the notification by implementing the same functionality with + * {@link #addAction addAction()}. + * + * @param icon Resource ID of a drawable that represents the action. + * @param title Text describing the action. + * @param intent {@link android.app.PendingIntent} to be fired when the action is invoked. + */ + public Builder addAction(int icon, CharSequence title, PendingIntent intent) { + mActions.add(new Action(icon, title, intent)); + return this; + } + + /** + * Add an action to this notification. Actions are typically displayed by + * the system as a button adjacent to the notification content. + *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. To ensure that an action button's functionality is always available, first + * implement the functionality in the {@link android.app.Activity} that starts when a user + * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then + * enhance the notification by implementing the same functionality with + * {@link #addAction addAction()}. + * + * @param action The action to add. + */ + public Builder addAction(Action action) { + mActions.add(action); + return this; + } + + /** + * Add a rich notification style to be applied at build time. + *
+ * If the platform does not provide rich notification styles, this method has no effect. The + * user will always see the normal notification style. + * + * @param style Object responsible for modifying the notification style. + */ + public Builder setStyle(Style style) { + if (mStyle != style) { + mStyle = style; + if (mStyle != null) { + mStyle.setBuilder(this); + } + } + return this; + } + + /** + * Sets {@link Notification#color}. + * + * @param argb The accent color to use + * + * @return The same Builder. + */ + public Builder setColor(int argb) { + mColor = argb; + return this; + } + + /** + * Sets {@link Notification#visibility}. + * + * @param visibility One of {@link Notification#VISIBILITY_PRIVATE} (the default), + * {@link Notification#VISIBILITY_PUBLIC}, or + * {@link Notification#VISIBILITY_SECRET}. + */ + public Builder setVisibility(int visibility) { + mVisibility = visibility; + return this; + } + + /** + * Supply a replacement Notification whose contents should be shown in insecure contexts + * (i.e. atop the secure lockscreen). See {@link Notification#visibility} and + * {@link #VISIBILITY_PUBLIC}. + * + * @param n A replacement notification, presumably with some or all info redacted. + * @return The same Builder. + */ + public Builder setPublicVersion(Notification n) { + mPublicVersion = n; + return this; + } + + /** + * Apply an extender to this notification builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + + /** + * @deprecated Use {@link #build()} instead. + */ + @Deprecated + public Notification getNotification() { + return build(); + } + + /** + * Combine all of the options that have been set and return a new {@link Notification} + * object. + */ + public Notification build() { + return IMPL.build(this, getExtender()); + } + + /** + * @hide + */ + protected BuilderExtender getExtender() { + return new BuilderExtender(); + } + + protected static CharSequence limitCharSequenceLength(CharSequence cs) { + if (cs == null) return cs; + if (cs.length() > MAX_CHARSEQUENCE_LENGTH) { + cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH); + } + return cs; + } + } + + /** + * An object that can apply a rich notification style to a {@link Notification.Builder} + * object. + *
+ * If the platform does not provide rich notification styles, methods in this class have no + * effect. + */ + public static abstract class Style { + Builder mBuilder; + CharSequence mBigContentTitle; + CharSequence mSummaryText; + boolean mSummaryTextSet = false; + + public void setBuilder(Builder builder) { + if (mBuilder != builder) { + mBuilder = builder; + if (mBuilder != null) { + mBuilder.setStyle(this); + } + } + } + + public Notification build() { + Notification notification = null; + if (mBuilder != null) { + notification = mBuilder.build(); + } + return notification; + } + } + + /** + * Helper class for generating large-format notifications that include a large image attachment. + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *

+     * Notification notif = new Notification.Builder(mContext)
+     *     .setContentTitle("New photo from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_post)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigPictureStyle()
+     *         .bigPicture(aBigBitmap))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class BigPictureStyle extends Style { + Bitmap mPicture; + Bitmap mBigLargeIcon; + boolean mBigLargeIconSet; + + public BigPictureStyle() { + } + + public BigPictureStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public BigPictureStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public BigPictureStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Provide the bitmap to be used as the payload for the BigPicture notification. + */ + public BigPictureStyle bigPicture(Bitmap b) { + mPicture = b; + return this; + } + + /** + * Override the large icon when the big notification is shown. + */ + public BigPictureStyle bigLargeIcon(Bitmap b) { + mBigLargeIcon = b; + mBigLargeIconSet = true; + return this; + } + } + + /** + * Helper class for generating large-format notifications that include a lot of text. + * + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *
+     * Notification notif = new Notification.Builder(mContext)
+     *     .setContentTitle("New mail from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigTextStyle()
+     *         .bigText(aVeryLongString))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class BigTextStyle extends Style { + CharSequence mBigText; + + public BigTextStyle() { + } + + public BigTextStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public BigTextStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public BigTextStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Provide the longer text to be displayed in the big form of the + * template in place of the content text. + */ + public BigTextStyle bigText(CharSequence cs) { + mBigText = Builder.limitCharSequenceLength(cs); + return this; + } + } + + /** + * Helper class for generating large-format notifications that include a list of (up to 5) strings. + * + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *
+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle("5 New mails from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.InboxStyle()
+     *         .addLine(str1)
+     *         .addLine(str2)
+     *         .setContentTitle("")
+     *         .setSummaryText("+3 more"))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class InboxStyle extends Style { + ArrayList mTexts = new ArrayList(); + + public InboxStyle() { + } + + public InboxStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public InboxStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public InboxStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Append a line to the digest section of the Inbox notification. + */ + public InboxStyle addLine(CharSequence cs) { + mTexts.add(Builder.limitCharSequenceLength(cs)); + return this; + } + } + + /** + * Structure to encapsulate a named action that can be shown as part of this notification. + * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is + * selected by the user. Action buttons won't appear on platforms prior to Android 4.1. + *

+ * Apps should use {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} + * or {@link NotificationCompat.Builder#addAction(NotificationCompat.Action)} + * to attach actions. + */ + public static class Action extends NotificationCompatBase.Action { + private final Bundle mExtras; + private final RemoteInput[] mRemoteInputs; + + /** + * Small icon representing the action. + */ + public int icon; + /** + * Title of the action. + */ + public CharSequence title; + /** + * Intent to send when the user invokes this action. May be null, in which case the action + * may be rendered in a disabled presentation. + */ + public PendingIntent actionIntent; + + public Action(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle(), null); + } + + private Action(int icon, CharSequence title, PendingIntent intent, Bundle extras, + RemoteInput[] remoteInputs) { + this.icon = icon; + this.title = NotificationCompat.Builder.limitCharSequenceLength(title); + this.actionIntent = intent; + this.mExtras = extras != null ? extras : new Bundle(); + this.mRemoteInputs = remoteInputs; + } + + @Override + public int getIcon() { + return icon; + } + + @Override + public CharSequence getTitle() { + return title; + } + + @Override + public PendingIntent getActionIntent() { + return actionIntent; + } + + /** + * Get additional metadata carried around with this Action. + */ + @Override + public Bundle getExtras() { + return mExtras; + } + + /** + * Get the list of inputs to be collected from the user when this action is sent. + * May return null if no remote inputs were added. + */ + @Override + public RemoteInput[] getRemoteInputs() { + return mRemoteInputs; + } + + /** + * Builder class for {@link Action} objects. + */ + public static final class Builder { + private final int mIcon; + private final CharSequence mTitle; + private final PendingIntent mIntent; + private final Bundle mExtras; + private ArrayList mRemoteInputs; + + /** + * Construct a new builder for {@link Action} object. + * @param icon icon to show for this action + * @param title the title of the action + * @param intent the {@link PendingIntent} to fire when users trigger this action + */ + public Builder(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle()); + } + + /** + * Construct a new builder for {@link Action} object using the fields from an + * {@link Action}. + * @param action the action to read fields from. + */ + public Builder(Action action) { + this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras)); + } + + private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras) { + mIcon = icon; + mTitle = NotificationCompat.Builder.limitCharSequenceLength(title); + mIntent = intent; + mExtras = extras; + } + + /** + * Merge additional metadata into this builder. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see NotificationCompat.Action#getExtras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + *

The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Add an input to be collected from the user when this action is sent. + * Response values can be retrieved from the fired intent by using the + * {@link RemoteInput#getResultsFromIntent} function. + * @param remoteInput a {@link RemoteInput} to add to the action + * @return this object for method chaining + */ + public Builder addRemoteInput(RemoteInput remoteInput) { + if (mRemoteInputs == null) { + mRemoteInputs = new ArrayList(); + } + mRemoteInputs.add(remoteInput); + return this; + } + + /** + * Apply an extender to this action builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link Action} + * object. + * @return the built action + */ + public Action build() { + RemoteInput[] remoteInputs = mRemoteInputs != null + ? mRemoteInputs.toArray(new RemoteInput[mRemoteInputs.size()]) : null; + return new Action(mIcon, mTitle, mIntent, mExtras, remoteInputs); + } + } + + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on an action builder. + */ + public interface Extender { + /** + * Apply this extender to a notification action builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Wearable extender for notification actions. To add extensions to an action, + * create a new {@link NotificationCompat.Action.WearableExtender} object using + * the {@code WearableExtender()} constructor and apply it to a + * {@link NotificationCompat.Action.Builder} using + * {@link NotificationCompat.Action.Builder#extend}. + * + *

+         * NotificationCompat.Action action = new NotificationCompat.Action.Builder(
+         *         R.drawable.archive_all, "Archive all", actionIntent)
+         *         .extend(new NotificationCompat.Action.WearableExtender()
+         *                 .setAvailableOffline(false))
+         *         .build();
+ */ + public static final class WearableExtender implements Extender { + /** Notification action extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. + private static final String KEY_FLAGS = "flags"; + private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel"; + private static final String KEY_CONFIRM_LABEL = "confirmLabel"; + private static final String KEY_CANCEL_LABEL = "cancelLabel"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_AVAILABLE_OFFLINE = 0x1; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE; + + private int mFlags = DEFAULT_FLAGS; + + private CharSequence mInProgressLabel; + private CharSequence mConfirmLabel; + private CharSequence mCancelLabel; + + /** + * Create a {@link NotificationCompat.Action.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + /** + * Create a {@link NotificationCompat.Action.WearableExtender} by reading + * wearable options present in an existing notification action. + * @param action the notification action to inspect. + */ + public WearableExtender(Action action) { + Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS); + if (wearableBundle != null) { + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL); + mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL); + mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL); + } + } + + /** + * Apply wearable extensions to a notification action that is being built. This is + * typically called by the {@link NotificationCompat.Action.Builder#extend} + * method of {@link NotificationCompat.Action.Builder}. + */ + @Override + public Action.Builder extend(Action.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + if (mInProgressLabel != null) { + wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel); + } + if (mConfirmLabel != null) { + wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel); + } + if (mCancelLabel != null) { + wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mFlags = this.mFlags; + that.mInProgressLabel = this.mInProgressLabel; + that.mConfirmLabel = this.mConfirmLabel; + that.mCancelLabel = this.mCancelLabel; + return that; + } + + /** + * Set whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device + * is offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public WearableExtender setAvailableOffline(boolean availableOffline) { + setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline); + return this; + } + + /** + * Get whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device + * is offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public boolean isAvailableOffline() { + return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + + /** + * Set a label to display while the wearable is preparing to automatically execute the + * action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @param label the label to display while the action is being prepared to execute + * @return this object for method chaining + */ + public WearableExtender setInProgressLabel(CharSequence label) { + mInProgressLabel = label; + return this; + } + + /** + * Get the label to display while the wearable is preparing to automatically execute + * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @return the label to display while the action is being prepared to execute + */ + public CharSequence getInProgressLabel() { + return mInProgressLabel; + } + + /** + * Set a label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @param label the label to confirm the action should be executed + * @return this object for method chaining + */ + public WearableExtender setConfirmLabel(CharSequence label) { + mConfirmLabel = label; + return this; + } + + /** + * Get the label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @return the label to confirm the action should be executed + */ + public CharSequence getConfirmLabel() { + return mConfirmLabel; + } + + /** + * Set a label to display to cancel the action. + * This is usually an imperative verb, like "Cancel". + * + * @param label the label to display to cancel the action + * @return this object for method chaining + */ + public WearableExtender setCancelLabel(CharSequence label) { + mCancelLabel = label; + return this; + } + + /** + * Get the label to display to cancel the action. + * This is usually an imperative verb like "Cancel". + * + * @return the label to display to cancel the action + */ + public CharSequence getCancelLabel() { + return mCancelLabel; + } + } + + /** @hide */ + public static final Factory FACTORY = new Factory() { + @Override + public Action build(int icon, CharSequence title, + PendingIntent actionIntent, Bundle extras, + RemoteInputCompatBase.RemoteInput[] remoteInputs) { + return new Action(icon, title, actionIntent, extras, + (RemoteInput[]) remoteInputs); + } + + @Override + public Action[] newArray(int length) { + return new Action[length]; + } + }; + } + + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on a notification builder. + */ + public interface Extender { + /** + * Apply this extender to a notification builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Helper class to add wearable extensions to notifications. + *

See + * Creating Notifications + * for Android Wear for more information on how to use this class. + *

+ * To create a notification with wearable extensions: + *

    + *
  1. Create a {@link NotificationCompat.Builder}, setting any desired + * properties. + *
  2. Create a {@link NotificationCompat.WearableExtender}. + *
  3. Set wearable-specific properties using the + * {@code add} and {@code set} methods of {@link NotificationCompat.WearableExtender}. + *
  4. Call {@link NotificationCompat.Builder#extend} to apply the extensions to a + * notification. + *
  5. Post the notification to the notification + * system with the {@code NotificationManagerCompat.notify(...)} methods + * and not the {@code NotificationManager.notify(...)} methods. + *
+ * + *
+     * Notification notif = new NotificationCompat.Builder(mContext)
+     *         .setContentTitle("New mail from " + sender.toString())
+     *         .setContentText(subject)
+     *         .setSmallIcon(R.drawable.new_mail)
+     *         .extend(new NotificationCompat.WearableExtender()
+     *                 .setContentIcon(R.drawable.new_mail))
+     *         .build();
+     * NotificationManagerCompat.from(mContext).notify(0, notif);
+ * + *

Wearable extensions can be accessed on an existing notification by using the + * {@code WearableExtender(Notification)} constructor, + * and then using the {@code get} methods to access values. + * + *

+     * NotificationCompat.WearableExtender wearableExtender =
+     *         new NotificationCompat.WearableExtender(notification);
+     * List<Notification> pages = wearableExtender.getPages();
+ */ + public static final class WearableExtender implements Extender { + /** + * Sentinel value for an action index that is unset. + */ + public static final int UNSET_ACTION_INDEX = -1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification with + * default sizing. + *

For custom display notifications created using {@link #setDisplayIntent}, + * the default is {@link #SIZE_LARGE}. All other notifications size automatically based + * on their content. + */ + public static final int SIZE_DEFAULT = 0; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with an extra small size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_XSMALL = 1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a small size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_SMALL = 2; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a medium size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_MEDIUM = 3; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a large size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_LARGE = 4; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * full screen. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_FULL_SCREEN = 5; + + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a + * short amount of time when this notification is displayed on the screen. This + * is the default value. + */ + public static final int SCREEN_TIMEOUT_SHORT = 0; + + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on + * for a longer amount of time when this notification is displayed on the screen. + */ + public static final int SCREEN_TIMEOUT_LONG = -1; + + /** Notification extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. + private static final String KEY_ACTIONS = "actions"; + private static final String KEY_FLAGS = "flags"; + private static final String KEY_DISPLAY_INTENT = "displayIntent"; + private static final String KEY_PAGES = "pages"; + private static final String KEY_BACKGROUND = "background"; + private static final String KEY_CONTENT_ICON = "contentIcon"; + private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity"; + private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex"; + private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset"; + private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight"; + private static final String KEY_GRAVITY = "gravity"; + private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1; + private static final int FLAG_HINT_HIDE_ICON = 1 << 1; + private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2; + private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3; + private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE; + + private static final int DEFAULT_CONTENT_ICON_GRAVITY = GravityCompat.END; + private static final int DEFAULT_GRAVITY = Gravity.BOTTOM; + + private ArrayList mActions = new ArrayList(); + private int mFlags = DEFAULT_FLAGS; + private PendingIntent mDisplayIntent; + private ArrayList mPages = new ArrayList(); + private Bitmap mBackground; + private int mContentIcon; + private int mContentIconGravity = DEFAULT_CONTENT_ICON_GRAVITY; + private int mContentActionIndex = UNSET_ACTION_INDEX; + private int mCustomSizePreset = SIZE_DEFAULT; + private int mCustomContentHeight; + private int mGravity = DEFAULT_GRAVITY; + private int mHintScreenTimeout; + + /** + * Create a {@link NotificationCompat.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + public WearableExtender(Notification notif) { + Bundle extras = getExtras(notif); + Bundle wearableBundle = extras != null ? extras.getBundle(EXTRA_WEARABLE_EXTENSIONS) + : null; + if (wearableBundle != null) { + Action[] actions = IMPL.getActionsFromParcelableArrayList( + wearableBundle.getParcelableArrayList(KEY_ACTIONS)); + if (actions != null) { + Collections.addAll(mActions, actions); + } + + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mDisplayIntent = wearableBundle.getParcelable(KEY_DISPLAY_INTENT); + + Notification[] pages = getNotificationArrayFromBundle( + wearableBundle, KEY_PAGES); + if (pages != null) { + Collections.addAll(mPages, pages); + } + + mBackground = wearableBundle.getParcelable(KEY_BACKGROUND); + mContentIcon = wearableBundle.getInt(KEY_CONTENT_ICON); + mContentIconGravity = wearableBundle.getInt(KEY_CONTENT_ICON_GRAVITY, + DEFAULT_CONTENT_ICON_GRAVITY); + mContentActionIndex = wearableBundle.getInt(KEY_CONTENT_ACTION_INDEX, + UNSET_ACTION_INDEX); + mCustomSizePreset = wearableBundle.getInt(KEY_CUSTOM_SIZE_PRESET, + SIZE_DEFAULT); + mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT); + mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY); + mHintScreenTimeout = wearableBundle.getInt(KEY_HINT_SCREEN_TIMEOUT); + } + } + + /** + * Apply wearable extensions to a notification that is being built. This is typically + * called by the {@link NotificationCompat.Builder#extend} method of + * {@link NotificationCompat.Builder}. + */ + @Override + public NotificationCompat.Builder extend(NotificationCompat.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (!mActions.isEmpty()) { + wearableBundle.putParcelableArrayList(KEY_ACTIONS, + IMPL.getParcelableArrayListForActions(mActions.toArray( + new Action[mActions.size()]))); + } + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + if (mDisplayIntent != null) { + wearableBundle.putParcelable(KEY_DISPLAY_INTENT, mDisplayIntent); + } + if (!mPages.isEmpty()) { + wearableBundle.putParcelableArray(KEY_PAGES, mPages.toArray( + new Notification[mPages.size()])); + } + if (mBackground != null) { + wearableBundle.putParcelable(KEY_BACKGROUND, mBackground); + } + if (mContentIcon != 0) { + wearableBundle.putInt(KEY_CONTENT_ICON, mContentIcon); + } + if (mContentIconGravity != DEFAULT_CONTENT_ICON_GRAVITY) { + wearableBundle.putInt(KEY_CONTENT_ICON_GRAVITY, mContentIconGravity); + } + if (mContentActionIndex != UNSET_ACTION_INDEX) { + wearableBundle.putInt(KEY_CONTENT_ACTION_INDEX, + mContentActionIndex); + } + if (mCustomSizePreset != SIZE_DEFAULT) { + wearableBundle.putInt(KEY_CUSTOM_SIZE_PRESET, mCustomSizePreset); + } + if (mCustomContentHeight != 0) { + wearableBundle.putInt(KEY_CUSTOM_CONTENT_HEIGHT, mCustomContentHeight); + } + if (mGravity != DEFAULT_GRAVITY) { + wearableBundle.putInt(KEY_GRAVITY, mGravity); + } + if (mHintScreenTimeout != 0) { + wearableBundle.putInt(KEY_HINT_SCREEN_TIMEOUT, mHintScreenTimeout); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mActions = new ArrayList(this.mActions); + that.mFlags = this.mFlags; + that.mDisplayIntent = this.mDisplayIntent; + that.mPages = new ArrayList(this.mPages); + that.mBackground = this.mBackground; + that.mContentIcon = this.mContentIcon; + that.mContentIconGravity = this.mContentIconGravity; + that.mContentActionIndex = this.mContentActionIndex; + that.mCustomSizePreset = this.mCustomSizePreset; + that.mCustomContentHeight = this.mCustomContentHeight; + that.mGravity = this.mGravity; + that.mHintScreenTimeout = this.mHintScreenTimeout; + return that; + } + + /** + * Add a wearable action to this notification. + * + *

When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link NotificationCompat.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param action the action to add to this notification + * @return this object for method chaining + * @see NotificationCompat.Action + */ + public WearableExtender addAction(Action action) { + mActions.add(action); + return this; + } + + /** + * Adds wearable actions to this notification. + * + *

When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link NotificationCompat.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param actions the actions to add to this notification + * @return this object for method chaining + * @see NotificationCompat.Action + */ + public WearableExtender addActions(List actions) { + mActions.addAll(actions); + return this; + } + + /** + * Clear all wearable actions present on this builder. + * @return this object for method chaining. + * @see #addAction + */ + public WearableExtender clearActions() { + mActions.clear(); + return this; + } + + /** + * Get the wearable actions present on this notification. + */ + public List getActions() { + return mActions; + } + + /** + * Set an intent to launch inside of an activity view when displaying + * this notification. The {@link PendingIntent} provided should be for an activity. + * + *

+         * Intent displayIntent = new Intent(context, MyDisplayActivity.class);
+         * PendingIntent displayPendingIntent = PendingIntent.getActivity(context,
+         *         0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+         * Notification notif = new NotificationCompat.Builder(context)
+         *         .extend(new NotificationCompat.WearableExtender()
+         *                 .setDisplayIntent(displayPendingIntent)
+         *                 .setCustomSizePreset(NotificationCompat.WearableExtender.SIZE_MEDIUM))
+         *         .build();
+ * + *

The activity to launch needs to allow embedding, must be exported, and + * should have an empty task affinity. It is also recommended to use the device + * default light theme. + * + *

Example AndroidManifest.xml entry: + *

+         * <activity android:name="com.example.MyDisplayActivity"
+         *     android:exported="true"
+         *     android:allowEmbedded="true"
+         *     android:taskAffinity=""
+         *     android:theme="@android:style/Theme.DeviceDefault.Light" />
+ * + * @param intent the {@link PendingIntent} for an activity + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getDisplayIntent + */ + public WearableExtender setDisplayIntent(PendingIntent intent) { + mDisplayIntent = intent; + return this; + } + + /** + * Get the intent to launch inside of an activity view when displaying this + * notification. This {@code PendingIntent} should be for an activity. + */ + public PendingIntent getDisplayIntent() { + return mDisplayIntent; + } + + /** + * Add an additional page of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param page the notification to add as another page + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getPages + */ + public WearableExtender addPage(Notification page) { + mPages.add(page); + return this; + } + + /** + * Add additional pages of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param pages a list of notifications + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getPages + */ + public WearableExtender addPages(List pages) { + mPages.addAll(pages); + return this; + } + + /** + * Clear all additional pages present on this builder. + * @return this object for method chaining. + * @see #addPage + */ + public WearableExtender clearPages() { + mPages.clear(); + return this; + } + + /** + * Get the array of additional pages of content for displaying this notification. The + * current notification forms the first page, and elements within this array form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * @return the pages for this notification + */ + public List getPages() { + return mPages; + } + + /** + * Set a background image to be displayed behind the notification content. + * Contrary to the {@link NotificationCompat.BigPictureStyle}, this background + * will work with any notification style. + * + * @param background the background bitmap + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getBackground + */ + public WearableExtender setBackground(Bitmap background) { + mBackground = background; + return this; + } + + /** + * Get a background image to be displayed behind the notification content. + * Contrary to the {@link NotificationCompat.BigPictureStyle}, this background + * will work with any notification style. + * + * @return the background image + * @see NotificationCompat.WearableExtender#setBackground + */ + public Bitmap getBackground() { + return mBackground; + } + + /** + * Set an icon that goes with the content of this notification. + */ + public WearableExtender setContentIcon(int icon) { + mContentIcon = icon; + return this; + } + + /** + * Get an icon that goes with the content of this notification. + */ + public int getContentIcon() { + return mContentIcon; + } + + /** + * Set the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #setContentIcon + */ + public WearableExtender setContentIconGravity(int contentIconGravity) { + mContentIconGravity = contentIconGravity; + return this; + } + + /** + * Get the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #getContentIcon + */ + public int getContentIconGravity() { + return mContentIconGravity; + } + + /** + * Set an action from this notification's actions to be clickable with the content of + * this notification. This action will no longer display separately from the + * notification's content. + * + *

For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + * @param actionIndex The index of the action to hoist onto the current notification page. + * If wearable actions were added to the main notification, this index + * will apply to that list, otherwise it will apply to the regular + * actions list. + */ + public WearableExtender setContentAction(int actionIndex) { + mContentActionIndex = actionIndex; + return this; + } + + /** + * Get the index of the notification action, if any, that was specified as being clickable + * with the content of this notification. This action will no longer display separately + * from the notification's content. + * + *

For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + *

If wearable specific actions were added to the main notification, this index will + * apply to that list, otherwise it will apply to the regular actions list. + * + * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected. + */ + public int getContentAction() { + return mContentActionIndex; + } + + /** + * Set the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public WearableExtender setGravity(int gravity) { + mGravity = gravity; + return this; + } + + /** + * Get the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public int getGravity() { + return mGravity; + } + + /** + * Set the custom size preset for the display of this notification out of the available + * presets found in {@link NotificationCompat.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + *

Some custom size presets are only applicable for custom display notifications created + * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. Check the + * documentation for the preset in question. See also + * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}. + */ + public WearableExtender setCustomSizePreset(int sizePreset) { + mCustomSizePreset = sizePreset; + return this; + } + + /** + * Get the custom size preset for the display of this notification out of the available + * presets found in {@link NotificationCompat.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + *

Some custom size presets are only applicable for custom display notifications created + * using {@link #setDisplayIntent}. Check the documentation for the preset in question. + * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}. + */ + public int getCustomSizePreset() { + return mCustomSizePreset; + } + + /** + * Set the custom height in pixels for the display of this notification's content. + *

This option is only available for custom display notifications created + * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. See also + * {@link NotificationCompat.WearableExtender#setCustomSizePreset} and + * {@link #getCustomContentHeight}. + */ + public WearableExtender setCustomContentHeight(int height) { + mCustomContentHeight = height; + return this; + } + + /** + * Get the custom height in pixels for the display of this notification's content. + *

This option is only available for custom display notifications created + * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and + * {@link #setCustomContentHeight}. + */ + public int getCustomContentHeight() { + return mCustomContentHeight; + } + + /** + * Set whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public WearableExtender setStartScrollBottom(boolean startScrollBottom) { + setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom); + return this; + } + + /** + * Get whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public boolean getStartScrollBottom() { + return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0; + } + + /** + * Set whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public WearableExtender setContentIntentAvailableOffline( + boolean contentIntentAvailableOffline) { + setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline); + return this; + } + + /** + * Get whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public boolean getContentIntentAvailableOffline() { + return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0; + } + + /** + * Set a hint that this notification's icon should not be displayed. + * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise. + * @return this object for method chaining + */ + public WearableExtender setHintHideIcon(boolean hintHideIcon) { + setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon); + return this; + } + + /** + * Get a hint that this notification's icon should not be displayed. + * @return {@code true} if this icon should not be displayed, false otherwise. + * The default value is {@code false} if this was never set. + */ + public boolean getHintHideIcon() { + return (mFlags & FLAG_HINT_HIDE_ICON) != 0; + } + + /** + * Set a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link #addPage}. + */ + public WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) { + setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly); + return this; + } + + /** + * Get a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link NotificationCompat.WearableExtender#addPage}. + */ + public boolean getHintShowBackgroundOnly() { + return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0; + } + + /** + * Set a hint that this notification's background should not be clipped if possible, + * and should instead be resized to fully display on the screen, retaining the aspect + * ratio of the image. This can be useful for images like barcodes or qr codes. + * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible. + * @return this object for method chaining + */ + public WearableExtender setHintAvoidBackgroundClipping( + boolean hintAvoidBackgroundClipping) { + setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping); + return this; + } + + /** + * Get a hint that this notification's background should not be clipped if possible, + * and should instead be resized to fully display on the screen, retaining the aspect + * ratio of the image. This can be useful for images like barcodes or qr codes. + * @return {@code true} if it's ok if the background is clipped on the screen, false + * otherwise. The default value is {@code false} if this was never set. + */ + public boolean getHintAvoidBackgroundClipping() { + return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0; + } + + /** + * Set a hint that the screen should remain on for at least this duration when + * this notification is displayed on the screen. + * @param timeout The requested screen timeout in milliseconds. Can also be either + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + * @return this object for method chaining + */ + public WearableExtender setHintScreenTimeout(int timeout) { + mHintScreenTimeout = timeout; + return this; + } + + /** + * Get the duration, in milliseconds, that the screen should remain on for + * when this notification is displayed. + * @return the duration in milliseconds if > 0, or either one of the sentinel values + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + */ + public int getHintScreenTimeout() { + return mHintScreenTimeout; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + } + + /** + *

Helper class to add Android Auto extensions to notifications. To create a notification + * with car extensions: + * + *

    + *
  1. Create an {@link NotificationCompat.Builder}, setting any desired + * properties. + *
  2. Create a {@link CarExtender}. + *
  3. Set car-specific properties using the {@code add} and {@code set} methods of + * {@link CarExtender}. + *
  4. Call {@link android.support.v4.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)} + * to apply the extensions to a notification. + *
  5. Post the notification to the notification system with the + * {@code NotificationManagerCompat.notify(...)} methods and not the + * {@code NotificationManager.notify(...)} methods. + *
+ * + *
+     * Notification notification = new NotificationCompat.Builder(context)
+     *         ...
+     *         .extend(new CarExtender()
+     *                 .set*(...))
+     *         .build();
+     * 
+ * + *

Car extensions can be accessed on an existing notification by using the + * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods + * to access values. + */ + public static final class CarExtender implements Extender { + private static final String TAG = "CarExtender"; + + private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS"; + private static final String EXTRA_LARGE_ICON = "large_icon"; + private static final String EXTRA_CONVERSATION = "car_conversation"; + private static final String EXTRA_COLOR = "app_color"; + + private Bitmap mLargeIcon; + private UnreadConversation mUnreadConversation; + private int mColor = NotificationCompat.COLOR_DEFAULT; + + /** + * Create a {@link CarExtender} with default options. + */ + public CarExtender() { + } + + /** + * Create a {@link CarExtender} from the CarExtender options of an existing Notification. + * + * @param notif The notification from which to copy options. + */ + public CarExtender(Notification notif) { + if (Build.VERSION.SDK_INT < 21) { + return; + } + + Bundle carBundle = getExtras(notif)==null ? + null : getExtras(notif).getBundle(EXTRA_CAR_EXTENDER); + if (carBundle != null) { + mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON); + mColor = carBundle.getInt(EXTRA_COLOR, NotificationCompat.COLOR_DEFAULT); + + Bundle b = carBundle.getBundle(EXTRA_CONVERSATION); + mUnreadConversation = (UnreadConversation) IMPL.getUnreadConversationFromBundle( + b, UnreadConversation.FACTORY, RemoteInput.FACTORY); + } + } + + /** + * Apply car extensions to a notification that is being built. This is typically called by + * the {@link android.support.v4.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)} + * method of {@link NotificationCompat.Builder}. + */ + @Override + public NotificationCompat.Builder extend(NotificationCompat.Builder builder) { + if (Build.VERSION.SDK_INT < 21) { + return builder; + } + + Bundle carExtensions = new Bundle(); + + if (mLargeIcon != null) { + carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); + } + if (mColor != NotificationCompat.COLOR_DEFAULT) { + carExtensions.putInt(EXTRA_COLOR, mColor); + } + + if (mUnreadConversation != null) { + Bundle b = IMPL.getBundleForUnreadConversation(mUnreadConversation); + carExtensions.putBundle(EXTRA_CONVERSATION, b); + } + + builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions); + return builder; + } + + /** + * Sets the accent color to use when Android Auto presents the notification. + * + * Android Auto uses the color set with {@link android.support.v4.app.NotificationCompat.Builder#setColor(int)} + * to accent the displayed notification. However, not all colors are acceptable in an + * automotive setting. This method can be used to override the color provided in the + * notification in such a situation. + */ + public CarExtender setColor(int color) { + mColor = color; + return this; + } + + /** + * Gets the accent color. + * + * @see setColor + */ + public int getColor() { + return mColor; + } + + /** + * Sets the large icon of the car notification. + * + * If no large icon is set in the extender, Android Auto will display the icon + * specified by {@link android.support.v4.app.NotificationCompat.Builder#setLargeIcon(android.graphics.Bitmap)} + * + * @param largeIcon The large icon to use in the car notification. + * @return This object for method chaining. + */ + public CarExtender setLargeIcon(Bitmap largeIcon) { + mLargeIcon = largeIcon; + return this; + } + + /** + * Gets the large icon used in this car notification, or null if no icon has been set. + * + * @return The large icon for the car notification. + * @see CarExtender#setLargeIcon + */ + public Bitmap getLargeIcon() { + return mLargeIcon; + } + + /** + * Sets the unread conversation in a message notification. + * + * @param unreadConversation The unread part of the conversation this notification conveys. + * @return This object for method chaining. + */ + public CarExtender setUnreadConversation(UnreadConversation unreadConversation) { + mUnreadConversation = unreadConversation; + return this; + } + + /** + * Returns the unread conversation conveyed by this notification. + * @see #setUnreadConversation(UnreadConversation) + */ + public UnreadConversation getUnreadConversation() { + return mUnreadConversation; + } + + /** + * A class which holds the unread messages from a conversation. + */ + public static class UnreadConversation extends NotificationCompatBase.UnreadConversation { + private final String[] mMessages; + private final RemoteInput mRemoteInput; + private final PendingIntent mReplyPendingIntent; + private final PendingIntent mReadPendingIntent; + private final String[] mParticipants; + private final long mLatestTimestamp; + + UnreadConversation(String[] messages, RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp) { + mMessages = messages; + mRemoteInput = remoteInput; + mReadPendingIntent = readPendingIntent; + mReplyPendingIntent = replyPendingIntent; + mParticipants = participants; + mLatestTimestamp = latestTimestamp; + } + + /** + * Gets the list of messages conveyed by this notification. + */ + @Override + public String[] getMessages() { + return mMessages; + } + + /** + * Gets the remote input that will be used to convey the response to a message list, or + * null if no such remote input exists. + */ + @Override + public RemoteInput getRemoteInput() { + return mRemoteInput; + } + + /** + * Gets the pending intent that will be triggered when the user replies to this + * notification. + */ + @Override + public PendingIntent getReplyPendingIntent() { + return mReplyPendingIntent; + } + + /** + * Gets the pending intent that Android Auto will send after it reads aloud all messages + * in this object's message list. + */ + @Override + public PendingIntent getReadPendingIntent() { + return mReadPendingIntent; + } + + /** + * Gets the participants in the conversation. + */ + @Override + public String[] getParticipants() { + return mParticipants; + } + + /** + * Gets the firs participant in the conversation. + */ + @Override + public String getParticipant() { + return mParticipants.length > 0 ? mParticipants[0] : null; + } + + /** + * Gets the timestamp of the conversation. + */ + @Override + public long getLatestTimestamp() { + return mLatestTimestamp; + } + + /** @hide */ + static final Factory FACTORY = new Factory() { + @Override + public UnreadConversation build( + String[] messages, RemoteInputCompatBase.RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp) { + return new UnreadConversation( + messages, (RemoteInput) remoteInput, replyPendingIntent, + readPendingIntent, + participants, latestTimestamp); + } + }; + + /** + * Builder class for {@link CarExtender.UnreadConversation} objects. + */ + public static class Builder { + private final List mMessages = new ArrayList(); + private final String mParticipant; + private RemoteInput mRemoteInput; + private PendingIntent mReadPendingIntent; + private PendingIntent mReplyPendingIntent; + private long mLatestTimestamp; + + /** + * Constructs a new builder for {@link CarExtender.UnreadConversation}. + * + * @param name The name of the other participant in the conversation. + */ + public Builder(String name) { + mParticipant = name; + } + + /** + * Appends a new unread message to the list of messages for this conversation. + * + * The messages should be added from oldest to newest. + * + * @param message The text of the new unread message. + * @return This object for method chaining. + */ + public Builder addMessage(String message) { + mMessages.add(message); + return this; + } + + /** + * Sets the pending intent and remote input which will convey the reply to this + * notification. + * + * @param pendingIntent The pending intent which will be triggered on a reply. + * @param remoteInput The remote input parcelable which will carry the reply. + * @return This object for method chaining. + * + * @see CarExtender.UnreadConversation#getRemoteInput + * @see CarExtender.UnreadConversation#getReplyPendingIntent + */ + public Builder setReplyAction( + PendingIntent pendingIntent, RemoteInput remoteInput) { + mRemoteInput = remoteInput; + mReplyPendingIntent = pendingIntent; + + return this; + } + + /** + * Sets the pending intent that will be sent once the messages in this notification + * are read. + * + * @param pendingIntent The pending intent to use. + * @return This object for method chaining. + */ + public Builder setReadPendingIntent(PendingIntent pendingIntent) { + mReadPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the timestamp of the most recent message in an unread conversation. + * + * If a messaging notification has been posted by your application and has not + * yet been cancelled, posting a later notification with the same id and tag + * but without a newer timestamp may result in Android Auto not displaying a + * heads up notification for the later notification. + * + * @param timestamp The timestamp of the most recent message in the conversation. + * @return This object for method chaining. + */ + public Builder setLatestTimestamp(long timestamp) { + mLatestTimestamp = timestamp; + return this; + } + + /** + * Builds a new unread conversation object. + * + * @return The new unread conversation object. + */ + public UnreadConversation build() { + String[] messages = mMessages.toArray(new String[mMessages.size()]); + String[] participants = { mParticipant }; + return new UnreadConversation(messages, mRemoteInput, mReplyPendingIntent, + mReadPendingIntent, participants, mLatestTimestamp); + } + } + } + } + + + /** + * Get an array of Notification objects from a parcelable array bundle field. + * Update the bundle to have a typed array so fetches in the future don't need + * to do an array copy. + */ + private static Notification[] getNotificationArrayFromBundle(Bundle bundle, String key) { + Parcelable[] array = bundle.getParcelableArray(key); + if (array instanceof Notification[] || array == null) { + return (Notification[]) array; + } + Notification[] typedArray = new Notification[array.length]; + for (int i = 0; i < array.length; i++) { + typedArray[i] = (Notification) array[i]; + } + bundle.putParcelableArray(key, typedArray); + return typedArray; + } + + /** + * Gets the {@link Notification#extras} field from a notification in a backwards + * compatible manner. Extras field was supported from JellyBean (Api level 16) + * forwards. This function will return null on older api levels. + */ + public static Bundle getExtras(Notification notif) { + return IMPL.getExtras(notif); + } + + /** + * Get the number of actions in this notification in a backwards compatible + * manner. Actions were supported from JellyBean (Api level 16) forwards. + */ + public static int getActionCount(Notification notif) { + return IMPL.getActionCount(notif); + } + + /** + * Get an action on this notification in a backwards compatible + * manner. Actions were supported from JellyBean (Api level 16) forwards. + * @param notif The notification to inspect. + * @param actionIndex The index of the action to retrieve. + */ + public static Action getAction(Notification notif, int actionIndex) { + return IMPL.getAction(notif, actionIndex); + } + + /** + * Get the category of this notification in a backwards compatible + * manner. + * @param notif The notification to inspect. + */ + public static String getCategory(Notification notif) { + return IMPL.getCategory(notif); + } + + /** + * Get whether or not this notification is only relevant to the current device. + * + *

Some notifications can be bridged to other devices for remote display. + * If this hint is set, it is recommend that this notification not be bridged. + */ + public static boolean getLocalOnly(Notification notif) { + return IMPL.getLocalOnly(notif); + } + + /** + * Get the key used to group this notification into a cluster or stack + * with other notifications on devices which support such rendering. + */ + public static String getGroup(Notification notif) { + return IMPL.getGroup(notif); + } + + /** + * Get whether this notification to be the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link Builder#setGroup}. + * @return Whether this notification is a group summary. + */ + public static boolean isGroupSummary(Notification notif) { + return IMPL.isGroupSummary(notif); + } + + /** + * Get a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + *

This sort key can also be used to order members of a notification group. See + * {@link Builder#setGroup}. + * + * @see String#compareTo(String) + */ + public static String getSortKey(Notification notif) { + return IMPL.getSortKey(notif); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java new file mode 100644 index 00000000000..fcd75763c5e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java @@ -0,0 +1,199 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.RemoteViews; + +import java.util.ArrayList; + +class NotificationCompatApi20 { + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private Bundle mExtras; + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + ArrayList people, Bundle extras, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate) + .setLocalOnly(localOnly) + .setGroup(groupKey) + .setGroupSummary(groupSummary) + .setSortKey(sortKey); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (people != null && !people.isEmpty()) { + mExtras.putStringArray(Notification.EXTRA_PEOPLE, + people.toArray(new String[people.size()])); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + NotificationCompatApi20.addAction(b, action); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + b.setExtras(mExtras); + return b.build(); + } + } + + public static void addAction(Notification.Builder b, NotificationCompatBase.Action action) { + Notification.Action.Builder actionBuilder = new Notification.Action.Builder( + action.getIcon(), action.getTitle(), action.getActionIntent()); + if (action.getRemoteInputs() != null) { + for (RemoteInput remoteInput : RemoteInputCompatApi20.fromCompat( + action.getRemoteInputs())) { + actionBuilder.addRemoteInput(remoteInput); + } + } + if (action.getExtras() != null) { + actionBuilder.addExtras(action.getExtras()); + } + b.addAction(actionBuilder.build()); + } + + public static NotificationCompatBase.Action getAction(Notification notif, + int actionIndex, NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return getActionCompatFromAction(notif.actions[actionIndex], actionFactory, remoteInputFactory); + } + + private static NotificationCompatBase.Action getActionCompatFromAction( + Notification.Action action, NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + RemoteInputCompatBase.RemoteInput[] remoteInputs = RemoteInputCompatApi20.toCompat( + action.getRemoteInputs(), remoteInputFactory); + return actionFactory.build(action.icon, action.title, action.actionIntent, + action.getExtras(), remoteInputs); + } + + private static Notification.Action getActionFromActionCompat( + NotificationCompatBase.Action actionCompat) { + Notification.Action.Builder actionBuilder = new Notification.Action.Builder( + actionCompat.getIcon(), actionCompat.getTitle(), actionCompat.getActionIntent()) + .addExtras(actionCompat.getExtras()); + RemoteInputCompatBase.RemoteInput[] remoteInputCompats = actionCompat.getRemoteInputs(); + if (remoteInputCompats != null) { + RemoteInput[] remoteInputs = RemoteInputCompatApi20.fromCompat(remoteInputCompats); + for (RemoteInput remoteInput : remoteInputs) { + actionBuilder.addRemoteInput(remoteInput); + } + } + return actionBuilder.build(); + } + + /** + * Get a list of notification compat actions by parsing actions stored within a list of + * parcelables using the {@link Bundle#getParcelableArrayList} function in the same + * manner that framework code would do so. In API20, Using Action parcelable directly + * is correct. + */ + public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList( + ArrayList parcelables, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (parcelables == null) { + return null; + } + NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size()); + for (int i = 0; i < actions.length; i++) { + Notification.Action action = (Notification.Action) parcelables.get(i); + actions[i] = getActionCompatFromAction(action, actionFactory, remoteInputFactory); + } + return actions; + } + + /** + * Get an array list of parcelables, suitable for {@link Bundle#putParcelableArrayList}, + * that matches what framework code would do to store an actions list in this way. In API20, + * action parcelables were directly placed as entries in the array list. + */ + public static ArrayList getParcelableArrayListForActions( + NotificationCompatBase.Action[] actions) { + if (actions == null) { + return null; + } + ArrayList parcelables = new ArrayList(actions.length); + for (NotificationCompatBase.Action action : actions) { + parcelables.add(getActionFromActionCompat(action)); + } + return parcelables; + } + + public static boolean getLocalOnly(Notification notif) { + return (notif.flags & Notification.FLAG_LOCAL_ONLY) != 0; + } + + public static String getGroup(Notification notif) { + return notif.getGroup(); + } + + public static boolean isGroupSummary(Notification notif) { + return (notif.flags & Notification.FLAG_GROUP_SUMMARY) != 0; + } + + public static String getSortKey(Notification notif) { + return notif.getSortKey(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java new file mode 100644 index 00000000000..dbce1db52c0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java @@ -0,0 +1,223 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.RemoteViews; + +import java.util.ArrayList; + +class NotificationCompatApi21 { + + public static final String CATEGORY_CALL = Notification.CATEGORY_CALL; + public static final String CATEGORY_MESSAGE = Notification.CATEGORY_MESSAGE; + public static final String CATEGORY_EMAIL = Notification.CATEGORY_EMAIL; + public static final String CATEGORY_EVENT = Notification.CATEGORY_EVENT; + public static final String CATEGORY_PROMO = Notification.CATEGORY_PROMO; + public static final String CATEGORY_ALARM = Notification.CATEGORY_ALARM; + public static final String CATEGORY_PROGRESS = Notification.CATEGORY_PROGRESS; + public static final String CATEGORY_SOCIAL = Notification.CATEGORY_SOCIAL; + public static final String CATEGORY_ERROR = Notification.CATEGORY_ERROR; + public static final String CATEGORY_TRANSPORT = Notification.CATEGORY_TRANSPORT; + public static final String CATEGORY_SYSTEM = Notification.CATEGORY_SYSTEM; + public static final String CATEGORY_SERVICE = Notification.CATEGORY_SERVICE; + public static final String CATEGORY_RECOMMENDATION = Notification.CATEGORY_RECOMMENDATION; + public static final String CATEGORY_STATUS = Notification.CATEGORY_STATUS; + + private static final String KEY_AUTHOR = "author"; + private static final String KEY_TEXT = "text"; + private static final String KEY_MESSAGES = "messages"; + private static final String KEY_REMOTE_INPUT = "remote_input"; + private static final String KEY_ON_REPLY = "on_reply"; + private static final String KEY_ON_READ = "on_read"; + private static final String KEY_PARTICIPANTS = "participants"; + private static final String KEY_TIMESTAMP = "timestamp"; + + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + String category, ArrayList people, Bundle extras, int color, + int visibility, Notification publicVersion, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate) + .setLocalOnly(localOnly) + .setExtras(extras) + .setGroup(groupKey) + .setGroupSummary(groupSummary) + .setSortKey(sortKey) + .setCategory(category) + .setColor(color) + .setVisibility(visibility) + .setPublicVersion(publicVersion); + for (String person: people) { + b.addPerson(person); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + NotificationCompatApi20.addAction(b, action); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + return b.build(); + } + } + + public static String getCategory(Notification notif) { + return notif.category; + } + + static Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + if (uc == null) { + return null; + } + Bundle b = new Bundle(); + String author = null; + if (uc.getParticipants() != null && uc.getParticipants().length > 1) { + author = uc.getParticipants()[0]; + } + Parcelable[] messages = new Parcelable[uc.getMessages().length]; + for (int i = 0; i < messages.length; i++) { + Bundle m = new Bundle(); + m.putString(KEY_TEXT, uc.getMessages()[i]); + m.putString(KEY_AUTHOR, author); + messages[i] = m; + } + b.putParcelableArray(KEY_MESSAGES, messages); + RemoteInputCompatBase.RemoteInput remoteInput = uc.getRemoteInput(); + if (remoteInput != null) { + b.putParcelable(KEY_REMOTE_INPUT, fromCompatRemoteInput(remoteInput)); + } + b.putParcelable(KEY_ON_REPLY, uc.getReplyPendingIntent()); + b.putParcelable(KEY_ON_READ, uc.getReadPendingIntent()); + b.putStringArray(KEY_PARTICIPANTS, uc.getParticipants()); + b.putLong(KEY_TIMESTAMP, uc.getLatestTimestamp()); + return b; + } + + static NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (b == null) { + return null; + } + Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES); + String[] messages = null; + if (parcelableMessages != null) { + String[] tmp = new String[parcelableMessages.length]; + boolean success = true; + for (int i = 0; i < tmp.length; i++) { + if (!(parcelableMessages[i] instanceof Bundle)) { + success = false; + break; + } + tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT); + if (tmp[i] == null) { + success = false; + break; + } + } + if (success) { + messages = tmp; + } else { + return null; + } + } + + PendingIntent onRead = b.getParcelable(KEY_ON_READ); + PendingIntent onReply = b.getParcelable(KEY_ON_REPLY); + + android.app.RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT); + + String[] participants = b.getStringArray(KEY_PARTICIPANTS); + if (participants == null || participants.length != 1) { + return null; + } + + + return factory.build( + messages, + remoteInput != null ? toCompatRemoteInput(remoteInput, remoteInputFactory) : null, + onReply, + onRead, + participants, b.getLong(KEY_TIMESTAMP)); + } + + private static android.app.RemoteInput fromCompatRemoteInput( + RemoteInputCompatBase.RemoteInput src) { + return new android.app.RemoteInput.Builder(src.getResultKey()) + .setLabel(src.getLabel()) + .setChoices(src.getChoices()) + .setAllowFreeFormInput(src.getAllowFreeFormInput()) + .addExtras(src.getExtras()) + .build(); + } + + private static RemoteInputCompatBase.RemoteInput toCompatRemoteInput( + android.app.RemoteInput remoteInput, + RemoteInputCompatBase.RemoteInput.Factory factory) { + return factory.build(remoteInput.getResultKey(), + remoteInput.getLabel(), + remoteInput.getChoices(), + remoteInput.getAllowFreeFormInput(), + remoteInput.getExtras()); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java new file mode 100644 index 00000000000..777a57f6ddb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java @@ -0,0 +1,57 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.os.Bundle; + +/** + * @hide + */ +public class NotificationCompatBase { + + public static abstract class Action { + public abstract int getIcon(); + public abstract CharSequence getTitle(); + public abstract PendingIntent getActionIntent(); + public abstract Bundle getExtras(); + public abstract RemoteInputCompatBase.RemoteInput[] getRemoteInputs(); + + public interface Factory { + Action build(int icon, CharSequence title, PendingIntent actionIntent, + Bundle extras, RemoteInputCompatBase.RemoteInput[] remoteInputs); + public Action[] newArray(int length); + } + } + + public static abstract class UnreadConversation { + abstract String[] getParticipants(); + abstract String getParticipant(); + abstract String[] getMessages(); + abstract RemoteInputCompatBase.RemoteInput getRemoteInput(); + abstract PendingIntent getReplyPendingIntent(); + abstract PendingIntent getReadPendingIntent(); + abstract long getLatestTimestamp(); + + public interface Factory { + UnreadConversation build(String[] messages, + RemoteInputCompatBase.RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java new file mode 100644 index 00000000000..6a2ee932cd2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java @@ -0,0 +1,70 @@ +/* + * 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. + */ + +package android.support.v4.app; + +/** + * Well-known extras used by {@link NotificationCompat} for backwards compatibility. + */ +public final class NotificationCompatExtras { + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification#FLAG_LOCAL_ONLY} field before it was available. + * If possible, use {@link NotificationCompat#getLocalOnly} to access this field. + */ + public static final String EXTRA_LOCAL_ONLY = NotificationCompatJellybean.EXTRA_LOCAL_ONLY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setGroup} before it was available. + * If possible, use {@link NotificationCompat#getGroup} to access this value. + */ + public static final String EXTRA_GROUP_KEY = NotificationCompatJellybean.EXTRA_GROUP_KEY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setGroupSummary} before it was available. + * If possible, use {@link NotificationCompat#isGroupSummary} to access this value. + */ + public static final String EXTRA_GROUP_SUMMARY = + NotificationCompatJellybean.EXTRA_GROUP_SUMMARY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setSortKey} before it was available. + * If possible, use {@link NotificationCompat#getSortKey} to access this value. + */ + public static final String EXTRA_SORT_KEY = NotificationCompatJellybean.EXTRA_SORT_KEY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification.Action#extras} field before it was available. + * If possible, use {@link NotificationCompat#getAction} to access this field. + */ + public static final String EXTRA_ACTION_EXTRAS = + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification.Action#getRemoteInputs} before the field + * was available. + * If possible, use {@link NotificationCompat.Action#getRemoteInputs to access this field. + */ + public static final String EXTRA_REMOTE_INPUTS = + NotificationCompatJellybean.EXTRA_REMOTE_INPUTS; + + private NotificationCompatExtras() {} +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java new file mode 100644 index 00000000000..8537ab79bf7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; + +class NotificationCompatGingerbread { + public static Notification add(Notification notification, Context context, + CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent, + PendingIntent fullScreenIntent) { + notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); + notification.fullScreenIntent = fullScreenIntent; + return notification; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java new file mode 100644 index 00000000000..3048e91f93b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +class NotificationCompatHoneycomb { + static Notification add(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon) { + Notification.Builder b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number); + + return b.getNotification(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java new file mode 100644 index 00000000000..0842451f072 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +class NotificationCompatIceCreamSandwich { + + public static class Builder implements NotificationBuilderWithBuilderAccessor { + + private Notification.Builder b; + + public Builder(Context context, Notification n, CharSequence contentTitle, + CharSequence contentText, CharSequence contentInfo, RemoteViews tickerView, + int number, PendingIntent contentIntent, PendingIntent fullScreenIntent, + Bitmap largeIcon, int progressMax, int progress, boolean progressIndeterminate) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setProgress(progressMax, progress, progressIndeterminate); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + return b.getNotification(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java new file mode 100644 index 00000000000..6f54c4b816a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java @@ -0,0 +1,406 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +class NotificationCompatJellybean { + public static final String TAG = "NotificationCompat"; + + // Extras keys used for Jellybean SDK and above. + static final String EXTRA_LOCAL_ONLY = "android.support.localOnly"; + static final String EXTRA_ACTION_EXTRAS = "android.support.actionExtras"; + static final String EXTRA_REMOTE_INPUTS = "android.support.remoteInputs"; + static final String EXTRA_GROUP_KEY = "android.support.groupKey"; + static final String EXTRA_GROUP_SUMMARY = "android.support.isGroupSummary"; + static final String EXTRA_SORT_KEY = "android.support.sortKey"; + static final String EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel"; + + // Bundle keys for storing action fields in a bundle + private static final String KEY_ICON = "icon"; + private static final String KEY_TITLE = "title"; + private static final String KEY_ACTION_INTENT = "actionIntent"; + private static final String KEY_EXTRAS = "extras"; + private static final String KEY_REMOTE_INPUTS = "remoteInputs"; + + private static final Object sExtrasLock = new Object(); + private static Field sExtrasField; + private static boolean sExtrasFieldAccessFailed; + + private static final Object sActionsLock = new Object(); + private static Class sActionClass; + private static Field sActionsField; + private static Field sActionIconField; + private static Field sActionTitleField; + private static Field sActionIntentField; + private static boolean sActionsAccessFailed; + + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private final Bundle mExtras; + private List mActionExtrasList = new ArrayList(); + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + Bundle extras, String groupKey, boolean groupSummary, String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (localOnly) { + mExtras.putBoolean(EXTRA_LOCAL_ONLY, true); + } + if (groupKey != null) { + mExtras.putString(EXTRA_GROUP_KEY, groupKey); + if (groupSummary) { + mExtras.putBoolean(EXTRA_GROUP_SUMMARY, true); + } else { + mExtras.putBoolean(EXTRA_USE_SIDE_CHANNEL, true); + } + } + if (sortKey != null) { + mExtras.putString(EXTRA_SORT_KEY, sortKey); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + mActionExtrasList.add(writeActionAndGetExtras(b, action)); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + public Notification build() { + Notification notif = b.build(); + // Merge in developer provided extras, but let the values already set + // for keys take precedence. + Bundle extras = getExtras(notif); + Bundle mergeBundle = new Bundle(mExtras); + for (String key : mExtras.keySet()) { + if (extras.containsKey(key)) { + mergeBundle.remove(key); + } + } + extras.putAll(mergeBundle); + SparseArray actionExtrasMap = buildActionExtrasMap(mActionExtrasList); + if (actionExtrasMap != null) { + // Add the action extras sparse array if any action was added with extras. + getExtras(notif).putSparseParcelableArray(EXTRA_ACTION_EXTRAS, actionExtrasMap); + } + return notif; + } + } + + public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, CharSequence bigText) { + Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle) + .bigText(bigText); + if (useSummary) { + style.setSummaryText(summaryText); + } + } + + public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon, + boolean bigLargeIconSet) { + Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle) + .bigPicture(bigPicture); + if (bigLargeIconSet) { + style.bigLargeIcon(bigLargeIcon); + } + if (useSummary) { + style.setSummaryText(summaryText); + } + } + + public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, ArrayList texts) { + Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle); + if (useSummary) { + style.setSummaryText(summaryText); + } + for (CharSequence text: texts) { + style.addLine(text); + } + } + + /** Return an SparseArray for action extras or null if none was needed. */ + public static SparseArray buildActionExtrasMap(List actionExtrasList) { + SparseArray actionExtrasMap = null; + for (int i = 0, count = actionExtrasList.size(); i < count; i++) { + Bundle actionExtras = actionExtrasList.get(i); + if (actionExtras != null) { + if (actionExtrasMap == null) { + actionExtrasMap = new SparseArray(); + } + actionExtrasMap.put(i, actionExtras); + } + } + return actionExtrasMap; + } + + /** + * Get the extras Bundle from a notification using reflection. Extras were present in + * Jellybean notifications, but the field was private until KitKat. + */ + public static Bundle getExtras(Notification notif) { + synchronized (sExtrasLock) { + if (sExtrasFieldAccessFailed) { + return null; + } + try { + if (sExtrasField == null) { + Field extrasField = Notification.class.getDeclaredField("extras"); + if (!Bundle.class.isAssignableFrom(extrasField.getType())) { + Log.e(TAG, "Notification.extras field is not of type Bundle"); + sExtrasFieldAccessFailed = true; + return null; + } + extrasField.setAccessible(true); + sExtrasField = extrasField; + } + Bundle extras = (Bundle) sExtrasField.get(notif); + if (extras == null) { + extras = new Bundle(); + sExtrasField.set(notif, extras); + } + return extras; + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification extras", e); + } catch (NoSuchFieldException e) { + Log.e(TAG, "Unable to access notification extras", e); + } + sExtrasFieldAccessFailed = true; + return null; + } + } + + public static NotificationCompatBase.Action readAction( + NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, + CharSequence title, PendingIntent actionIntent, Bundle extras) { + RemoteInputCompatBase.RemoteInput[] remoteInputs = null; + if (extras != null) { + remoteInputs = RemoteInputCompatJellybean.fromBundleArray( + BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS), + remoteInputFactory); + } + return factory.build(icon, title, actionIntent, extras, remoteInputs); + } + + public static Bundle writeActionAndGetExtras( + Notification.Builder builder, NotificationCompatBase.Action action) { + builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent()); + Bundle actionExtras = new Bundle(action.getExtras()); + if (action.getRemoteInputs() != null) { + actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS, + RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs())); + } + return actionExtras; + } + + public static int getActionCount(Notification notif) { + synchronized (sActionsLock) { + Object[] actionObjects = getActionObjectsLocked(notif); + return actionObjects != null ? actionObjects.length : 0; + } + } + + public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex, + NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + synchronized (sActionsLock) { + try { + Object actionObject = getActionObjectsLocked(notif)[actionIndex]; + Bundle actionExtras = null; + Bundle extras = getExtras(notif); + if (extras != null) { + SparseArray 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); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } + } + return null; + } + + private static Object[] getActionObjectsLocked(Notification notif) { + synchronized (sActionsLock) { + if (!ensureActionReflectionReadyLocked()) { + return null; + } + try { + return (Object[]) sActionsField.get(notif); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + return null; + } + } + } + + private static boolean ensureActionReflectionReadyLocked() { + if (sActionsAccessFailed) { + return false; + } + try { + if (sActionsField == null) { + sActionClass = Class.forName("android.app.Notification$Action"); + sActionIconField = sActionClass.getDeclaredField("icon"); + sActionTitleField = sActionClass.getDeclaredField("title"); + sActionIntentField = sActionClass.getDeclaredField("actionIntent"); + sActionsField = Notification.class.getDeclaredField("actions"); + sActionsField.setAccessible(true); + } + } catch (ClassNotFoundException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } catch (NoSuchFieldException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } + return !sActionsAccessFailed; + } + + public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList( + ArrayList parcelables, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (parcelables == null) { + return null; + } + NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size()); + for (int i = 0; i < actions.length; i++) { + actions[i] = getActionFromBundle((Bundle) parcelables.get(i), + actionFactory, remoteInputFactory); + } + return actions; + } + + private static NotificationCompatBase.Action getActionFromBundle(Bundle bundle, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return actionFactory.build( + bundle.getInt(KEY_ICON), + bundle.getCharSequence(KEY_TITLE), + bundle.getParcelable(KEY_ACTION_INTENT), + bundle.getBundle(KEY_EXTRAS), + RemoteInputCompatJellybean.fromBundleArray( + BundleUtil.getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS), + remoteInputFactory)); + } + + public static ArrayList getParcelableArrayListForActions( + NotificationCompatBase.Action[] actions) { + if (actions == null) { + return null; + } + ArrayList parcelables = new ArrayList(actions.length); + for (NotificationCompatBase.Action action : actions) { + parcelables.add(getBundleForAction(action)); + } + return parcelables; + } + + private static Bundle getBundleForAction(NotificationCompatBase.Action action) { + Bundle bundle = new Bundle(); + bundle.putInt(KEY_ICON, action.getIcon()); + bundle.putCharSequence(KEY_TITLE, action.getTitle()); + bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent()); + bundle.putBundle(KEY_EXTRAS, action.getExtras()); + bundle.putParcelableArray(KEY_REMOTE_INPUTS, RemoteInputCompatJellybean.toBundleArray( + action.getRemoteInputs())); + return bundle; + } + + public static boolean getLocalOnly(Notification notif) { + return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY); + } + + public static String getGroup(Notification n) { + return getExtras(n).getString(EXTRA_GROUP_KEY); + } + + public static boolean isGroupSummary(Notification n) { + return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY); + } + + public static String getSortKey(Notification n) { + return getExtras(n).getString(EXTRA_SORT_KEY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java new file mode 100644 index 00000000000..e2fd3b70317 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java @@ -0,0 +1,156 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import java.util.ArrayList; +import java.util.List; + +class NotificationCompatKitKat { + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private Bundle mExtras; + private List mActionExtrasList = new ArrayList(); + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + ArrayList people, Bundle extras, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (people != null && !people.isEmpty()) { + mExtras.putStringArray(Notification.EXTRA_PEOPLE, + people.toArray(new String[people.size()])); + } + if (localOnly) { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, true); + } + if (groupKey != null) { + mExtras.putString(NotificationCompatJellybean.EXTRA_GROUP_KEY, groupKey); + if (groupSummary) { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY, true); + } else { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL, true); + } + } + if (sortKey != null) { + mExtras.putString(NotificationCompatJellybean.EXTRA_SORT_KEY, sortKey); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + mActionExtrasList.add(NotificationCompatJellybean.writeActionAndGetExtras(b, action)); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + SparseArray actionExtrasMap = NotificationCompatJellybean.buildActionExtrasMap( + mActionExtrasList); + if (actionExtrasMap != null) { + // Add the action extras sparse array if any action was added with extras. + mExtras.putSparseParcelableArray( + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS, actionExtrasMap); + } + b.setExtras(mExtras); + return b.build(); + } + } + + public static Bundle getExtras(Notification notif) { + return notif.extras; + } + + public static int getActionCount(Notification notif) { + return notif.actions != null ? notif.actions.length : 0; + } + + public static NotificationCompatBase.Action getAction(Notification notif, + int actionIndex, NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + Notification.Action action = notif.actions[actionIndex]; + Bundle actionExtras = null; + SparseArray actionExtrasMap = notif.extras.getSparseParcelableArray( + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS); + if (actionExtrasMap != null) { + actionExtras = actionExtrasMap.get(actionIndex); + } + return NotificationCompatJellybean.readAction(factory, remoteInputFactory, + action.icon, action.title, action.actionIntent, actionExtras); + } + + public static boolean getLocalOnly(Notification notif) { + return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY); + } + + public static String getGroup(Notification notif) { + return notif.extras.getString(NotificationCompatJellybean.EXTRA_GROUP_KEY); + } + + public static boolean isGroupSummary(Notification notif) { + return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY); + } + + public static String getSortKey(Notification notif) { + return notif.extras.getString(NotificationCompatJellybean.EXTRA_SORT_KEY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java new file mode 100644 index 00000000000..ee4c1e69b03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java @@ -0,0 +1,118 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; + +/** + * Abstract service to receive side channel notifications sent from + * {@link android.support.v4.app.NotificationManagerCompat}. + * + *

To receive side channel notifications, extend this service and register it in your + * android manifest with an intent filter for the BIND_NOTIFICATION_SIDE_CHANNEL action. + * Note: you must also have an enabled + * {@link android.service.notification.NotificationListenerService} within your package. + * + *

Example AndroidManifest.xml addition: + *

+ * <service android:name="com.example.NotificationSideChannelService">
+ *     <intent-filter>
+ *         <action android:name="android.support.BIND_NOTIFICATION_SIDE_CHANNEL" />
+ *     </intent-filter>
+ * </service>
+ * + */ +public abstract class NotificationCompatSideChannelService extends Service { + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(NotificationManagerCompat.ACTION_BIND_SIDE_CHANNEL)) { + // Block side channel service connections if the current sdk has no need for + // side channeling. + if (Build.VERSION.SDK_INT > NotificationManagerCompat.MAX_SIDE_CHANNEL_SDK_VERSION) { + return null; + } + return new NotificationSideChannelStub(); + } + return null; + } + + /** + * Handle a side-channeled notification being posted. + */ + public abstract void notify(String packageName, int id, String tag, Notification notification); + + /** + * Handle a side-channelled notification being cancelled. + */ + public abstract void cancel(String packageName, int id, String tag); + + /** + * Handle the side-channelled cancelling of all notifications for a package. + */ + public abstract void cancelAll(String packageName); + + private class NotificationSideChannelStub extends INotificationSideChannel.Stub { + @Override + public void notify(String packageName, int id, String tag, Notification notification) + throws RemoteException { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.notify(packageName, id, tag, notification); + } finally { + restoreCallingIdentity(idToken); + } + } + + @Override + public void cancel(String packageName, int id, String tag) throws RemoteException { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.cancel(packageName, id, tag); + } finally { + restoreCallingIdentity(idToken); + } + } + + @Override + public void cancelAll(String packageName) { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.cancelAll(packageName); + } finally { + restoreCallingIdentity(idToken); + } + } + } + + private void checkPermission(int callingUid, String packageName) { + for (String validPackage : getPackageManager().getPackagesForUid(callingUid)) { + if (validPackage.equals(packageName)) { + return; + } + } + throw new SecurityException("NotificationSideChannelService: Uid " + callingUid + + " is not authorized for package " + packageName); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java new file mode 100644 index 00000000000..a8480071871 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java @@ -0,0 +1,628 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.DeadObjectException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Compatibility library for NotificationManager with fallbacks for older platforms. + * + *

To use this class, call the static function {@link #from} to get a + * {@link NotificationManagerCompat} object, and then call one of its + * methods to post or cancel notifications. + */ +public class NotificationManagerCompat { + private static final String TAG = "NotifManCompat"; + + /** + * Notification extras key: if set to true, the posted notification should use + * the side channel for delivery instead of using notification manager. + */ + public static final String EXTRA_USE_SIDE_CHANNEL = + NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL; + + /** + * Intent action to register for on a service to receive side channel + * notifications. The listening service must be in the same package as an enabled + * {@link android.service.notification.NotificationListenerService}. + */ + public static final String ACTION_BIND_SIDE_CHANNEL = + "android.support.BIND_NOTIFICATION_SIDE_CHANNEL"; + + /** + * Maximum sdk build version which needs support for side channeled notifications. + * Currently the only needed use is for side channeling group children before KITKAT_WATCH. + */ + static final int MAX_SIDE_CHANNEL_SDK_VERSION = 19; + + /** Base time delay for a side channel listener queue retry. */ + private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000; + /** Maximum retries for a side channel listener before dropping tasks. */ + private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6; + /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ + private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS = + "enabled_notification_listeners"; + private static final int SIDE_CHANNEL_BIND_FLAGS; + + /** Cache of enabled notification listener components */ + private static final Object sEnabledNotificationListenersLock = new Object(); + /** Guarded by {@link #sEnabledNotificationListenersLock} */ + private static String sEnabledNotificationListeners; + /** Guarded by {@link #sEnabledNotificationListenersLock} */ + private static Set sEnabledNotificationListenerPackages = new HashSet(); + + private final Context mContext; + private final NotificationManager mNotificationManager; + /** Lock for mutable static fields */ + private static final Object sLock = new Object(); + /** Guarded by {@link #sLock} */ + private static SideChannelManager sSideChannelManager; + + /** Get a {@link NotificationManagerCompat} instance for a provided context. */ + public static NotificationManagerCompat from(Context context) { + return new NotificationManagerCompat(context); + } + + private NotificationManagerCompat(Context context) { + mContext = context; + mNotificationManager = (NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE); + } + + private static final Impl IMPL; + + interface Impl { + void cancelNotification(NotificationManager notificationManager, String tag, int id); + + void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification); + + int getSideChannelBindFlags(); + } + + static class ImplBase implements Impl { + @Override + public void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + notificationManager.cancel(id); + } + + @Override + public void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + notificationManager.notify(id, notification); + } + + @Override + public int getSideChannelBindFlags() { + return Service.BIND_AUTO_CREATE; + } + } + + static class ImplEclair extends ImplBase { + @Override + public void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + NotificationManagerCompatEclair.cancelNotification(notificationManager, tag, id); + } + + @Override + public void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + NotificationManagerCompatEclair.postNotification(notificationManager, tag, id, + notification); + } + } + + static class ImplIceCreamSandwich extends ImplEclair { + @Override + public int getSideChannelBindFlags() { + return NotificationManagerCompatIceCreamSandwich.SIDE_CHANNEL_BIND_FLAGS; + } + } + + static { + if (Build.VERSION.SDK_INT >= 14) { + IMPL = new ImplIceCreamSandwich(); + } else if (Build.VERSION.SDK_INT >= 5) { + IMPL = new ImplEclair(); + } else { + IMPL = new ImplBase(); + } + SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags(); + } + + /** + * Cancel a previously shown notification. + * @param id the ID of the notification + */ + public void cancel(int id) { + cancel(null, id); + } + + /** + * Cancel a previously shown notification. + * @param tag the string identifier of the notification. + * @param id the ID of the notification + */ + public void cancel(String tag, int id) { + IMPL.cancelNotification(mNotificationManager, tag, id); + if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { + pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag)); + } + } + + /** Cancel all previously shown notifications. */ + public void cancelAll() { + mNotificationManager.cancelAll(); + if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { + pushSideChannelQueue(new CancelTask(mContext.getPackageName())); + } + } + + /** + * Post a notification to be shown in the status bar, stream, etc. + * @param id the ID of the notification + * @param notification the notification to post to the system + */ + public void notify(int id, Notification notification) { + notify(null, id, notification); + } + + /** + * Post a notification to be shown in the status bar, stream, etc. + * @param tag the string identifier for a notification. Can be {@code null}. + * @param id the ID of the notification. The pair (tag, id) must be unique within your app. + * @param notification the notification to post to the system + */ + public void notify(String tag, int id, Notification notification) { + if (useSideChannelForNotification(notification)) { + pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); + // Cancel this notification in notification manager if it just transitioned to being + // side channelled. + IMPL.cancelNotification(mNotificationManager, tag, id); + } else { + IMPL.postNotification(mNotificationManager, tag, id, notification); + } + } + + /** + * Get the set of packages that have an enabled notification listener component within them. + */ + public static Set getEnabledListenerPackages(Context context) { + final String enabledNotificationListeners = Settings.Secure.getString( + context.getContentResolver(), + SETTING_ENABLED_NOTIFICATION_LISTENERS); + // Parse the string again if it is different from the last time this method was called. + if (enabledNotificationListeners != null + && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) { + final String[] components = enabledNotificationListeners.split(":"); + Set packageNames = new HashSet(components.length); + for (String component : components) { + ComponentName componentName = ComponentName.unflattenFromString(component); + if (componentName != null) { + packageNames.add(componentName.getPackageName()); + } + } + synchronized (sEnabledNotificationListenersLock) { + sEnabledNotificationListenerPackages = packageNames; + sEnabledNotificationListeners = enabledNotificationListeners; + } + } + return sEnabledNotificationListenerPackages; + } + + /** + * Returns true if this notification should use the side channel for delivery. + */ + private static boolean useSideChannelForNotification(Notification notification) { + Bundle extras = NotificationCompat.getExtras(notification); + return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL); + } + + /** + * Push a notification task for distribution to notification side channels. + */ + private void pushSideChannelQueue(Task task) { + synchronized (sLock) { + if (sSideChannelManager == null) { + sSideChannelManager = new SideChannelManager(mContext.getApplicationContext()); + } + } + sSideChannelManager.queueTask(task); + } + + /** + * Helper class to manage a queue of pending tasks to send to notification side channel + * listeners. + */ + private static class SideChannelManager implements Handler.Callback, ServiceConnection { + private static final int MSG_QUEUE_TASK = 0; + private static final int MSG_SERVICE_CONNECTED = 1; + private static final int MSG_SERVICE_DISCONNECTED = 2; + private static final int MSG_RETRY_LISTENER_QUEUE = 3; + + private static final String KEY_BINDER = "binder"; + + private final Context mContext; + private final HandlerThread mHandlerThread; + private final Handler mHandler; + private final Map mRecordMap = + new HashMap(); + private Set mCachedEnabledPackages = new HashSet(); + + public SideChannelManager(Context context) { + mContext = context; + mHandlerThread = new HandlerThread("NotificationManagerCompat"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper(), this); + } + + /** + * Queue a new task to be sent to all listeners. This function can be called + * from any thread. + */ + public void queueTask(Task task) { + mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_QUEUE_TASK: + handleQueueTask((Task) msg.obj); + return true; + case MSG_SERVICE_CONNECTED: + ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj; + handleServiceConnected(event.componentName, event.iBinder); + return true; + case MSG_SERVICE_DISCONNECTED: + handleServiceDisconnected((ComponentName) msg.obj); + return true; + case MSG_RETRY_LISTENER_QUEUE: + handleRetryListenerQueue((ComponentName) msg.obj); + return true; + } + return false; + } + + private void handleQueueTask(Task task) { + updateListenerMap(); + for (ListenerRecord record : mRecordMap.values()) { + record.taskQueue.add(task); + processListenerQueue(record); + } + } + + private void handleServiceConnected(ComponentName componentName, IBinder iBinder) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + record.service = INotificationSideChannel.Stub.asInterface(iBinder); + record.retryCount = 0; + processListenerQueue(record); + } + } + + private void handleServiceDisconnected(ComponentName componentName) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + ensureServiceUnbound(record); + } + } + + private void handleRetryListenerQueue(ComponentName componentName) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + processListenerQueue(record); + } + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Connected to service " + componentName); + } + mHandler.obtainMessage(MSG_SERVICE_CONNECTED, + new ServiceConnectedEvent(componentName, iBinder)) + .sendToTarget(); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Disconnected from service " + componentName); + } + mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget(); + } + + /** + * Check the current list of enabled listener packages and update the records map + * accordingly. + */ + private void updateListenerMap() { + Set enabledPackages = getEnabledListenerPackages(mContext); + if (enabledPackages.equals(mCachedEnabledPackages)) { + // Short-circuit when the list of enabled packages has not changed. + return; + } + mCachedEnabledPackages = enabledPackages; + List resolveInfos = mContext.getPackageManager().queryIntentServices( + new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES); + Set enabledComponents = new HashSet(); + for (ResolveInfo resolveInfo : resolveInfos) { + if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) { + continue; + } + ComponentName componentName = new ComponentName( + resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + if (resolveInfo.serviceInfo.permission != null) { + Log.w(TAG, "Permission present on component " + componentName + + ", not adding listener record."); + continue; + } + enabledComponents.add(componentName); + } + // Ensure all enabled components have a record in the listener map. + for (ComponentName componentName : enabledComponents) { + if (!mRecordMap.containsKey(componentName)) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding listener record for " + componentName); + } + mRecordMap.put(componentName, new ListenerRecord(componentName)); + } + } + // Remove listener records that are no longer for enabled components. + Iterator> it = + mRecordMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (!enabledComponents.contains(entry.getKey())) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Removing listener record for " + entry.getKey()); + } + ensureServiceUnbound(entry.getValue()); + it.remove(); + } + } + } + + /** + * Ensure we are already attempting to bind to a service, or start a new binding if not. + * @return Whether the service bind attempt was successful. + */ + private boolean ensureServiceBound(ListenerRecord record) { + if (record.bound) { + return true; + } + Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName); + record.bound = mContext.bindService(intent, this, SIDE_CHANNEL_BIND_FLAGS); + if (record.bound) { + record.retryCount = 0; + } else { + Log.w(TAG, "Unable to bind to listener " + record.componentName); + mContext.unbindService(this); + } + return record.bound; + } + + /** + * Ensure we have unbound from a service. + */ + private void ensureServiceUnbound(ListenerRecord record) { + if (record.bound) { + mContext.unbindService(this); + record.bound = false; + } + record.service = null; + } + + /** + * Schedule a delayed retry to communicate with a listener service. + * After a maximum number of attempts (with exponential back-off), start + * dropping pending tasks for this listener. + */ + private void scheduleListenerRetry(ListenerRecord record) { + if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) { + return; + } + record.retryCount++; + if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) { + Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to " + + record.componentName + " after " + record.retryCount + " retries"); + record.taskQueue.clear(); + return; + } + int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1)); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Scheduling retry for " + delayMs + " ms"); + } + Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName); + mHandler.sendMessageDelayed(msg, delayMs); + } + + /** + * Perform a processing step for a listener. First check the bind state, then attempt + * to flush the task queue, and if an error is encountered, schedule a retry. + */ + private void processListenerQueue(ListenerRecord record) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Processing component " + record.componentName + ", " + + record.taskQueue.size() + " queued tasks"); + } + if (record.taskQueue.isEmpty()) { + return; + } + if (!ensureServiceBound(record) || record.service == null) { + // Ensure bind has started and that a service interface is ready to use. + scheduleListenerRetry(record); + return; + } + // Attempt to flush all items in the task queue. + while (true) { + Task task = record.taskQueue.peek(); + if (task == null) { + break; + } + try { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Sending task " + task); + } + task.send(record.service); + record.taskQueue.remove(); + } catch (DeadObjectException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Remote service has died: " + record.componentName); + } + break; + } catch (RemoteException e) { + Log.w(TAG, "RemoteException communicating with " + record.componentName, e); + break; + } + } + if (!record.taskQueue.isEmpty()) { + // Some tasks were not sent, meaning an error was encountered, schedule a retry. + scheduleListenerRetry(record); + } + } + + /** A per-side-channel-service listener state record */ + private static class ListenerRecord { + public final ComponentName componentName; + /** Whether the service is currently bound to. */ + public boolean bound = false; + /** The service stub provided by onServiceConnected */ + public INotificationSideChannel service; + /** Queue of pending tasks to send to this listener service */ + public LinkedList taskQueue = new LinkedList(); + /** Number of retries attempted while connecting to this listener service */ + public int retryCount = 0; + + public ListenerRecord(ComponentName componentName) { + this.componentName = componentName; + } + } + } + + private static class ServiceConnectedEvent { + final ComponentName componentName; + final IBinder iBinder; + + public ServiceConnectedEvent(ComponentName componentName, + final IBinder iBinder) { + this.componentName = componentName; + this.iBinder = iBinder; + } + } + + private interface Task { + public void send(INotificationSideChannel service) throws RemoteException; + } + + private static class NotifyTask implements Task { + final String packageName; + final int id; + final String tag; + final Notification notif; + + public NotifyTask(String packageName, int id, String tag, Notification notif) { + this.packageName = packageName; + this.id = id; + this.tag = tag; + this.notif = notif; + } + + @Override + public void send(INotificationSideChannel service) throws RemoteException { + service.notify(packageName, id, tag, notif); + } + + public String toString() { + StringBuilder sb = new StringBuilder("NotifyTask["); + sb.append("packageName:").append(packageName); + sb.append(", id:").append(id); + sb.append(", tag:").append(tag); + sb.append("]"); + return sb.toString(); + } + } + + private static class CancelTask implements Task { + final String packageName; + final int id; + final String tag; + final boolean all; + + public CancelTask(String packageName) { + this.packageName = packageName; + this.id = 0; + this.tag = null; + this.all = true; + } + + public CancelTask(String packageName, int id, String tag) { + this.packageName = packageName; + this.id = id; + this.tag = tag; + this.all = false; + } + + @Override + public void send(INotificationSideChannel service) throws RemoteException { + if (all) { + service.cancelAll(packageName); + } else { + service.cancel(packageName, id, tag); + } + } + + public String toString() { + StringBuilder sb = new StringBuilder("CancelTask["); + sb.append("packageName:").append(packageName); + sb.append(", id:").append(id); + sb.append(", tag:").append(tag); + sb.append(", all:").append(all); + sb.append("]"); + return sb.toString(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java new file mode 100644 index 00000000000..45d96e4b664 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.NotificationManager; + +class NotificationManagerCompatEclair { + static void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + notificationManager.cancel(tag, id); + } + + public static void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + notificationManager.notify(tag, id, notification); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java new file mode 100644 index 00000000000..f08817920ce --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Service; + +class NotificationManagerCompatIceCreamSandwich { + static final int SIDE_CHANNEL_BIND_FLAGS = Service.BIND_AUTO_CREATE + | Service.BIND_WAIVE_PRIORITY; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java new file mode 100644 index 00000000000..38ecad092dd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java @@ -0,0 +1,276 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +/** + * Helper for using the {@link android.app.RemoteInput} API + * introduced after API level 4 in a backwards compatible fashion. + */ +public class RemoteInput extends RemoteInputCompatBase.RemoteInput { + private static final String TAG = "RemoteInput"; + + /** Label used to denote the clip data type used for remote input transport */ + public static final String RESULTS_CLIP_LABEL = RemoteInputCompatJellybean.RESULTS_CLIP_LABEL; + + /** Extra added to a clip data intent object to hold the results bundle. */ + public static final String EXTRA_RESULTS_DATA = RemoteInputCompatJellybean.EXTRA_RESULTS_DATA; + + private final String mResultKey; + private final CharSequence mLabel; + private final CharSequence[] mChoices; + private final boolean mAllowFreeFormInput; + private final Bundle mExtras; + + RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, + boolean allowFreeFormInput, Bundle extras) { + this.mResultKey = resultKey; + this.mLabel = label; + this.mChoices = choices; + this.mAllowFreeFormInput = allowFreeFormInput; + this.mExtras = extras; + } + + /** + * Get the key that the result of this input will be set in from the Bundle returned by + * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent. + */ + public String getResultKey() { + return mResultKey; + } + + /** + * Get the label to display to users when collecting this input. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Get possible input choices. This can be {@code null} if there are no choices to present. + */ + public CharSequence[] getChoices() { + return mChoices; + } + + /** + * Get whether or not users can provide an arbitrary value for + * input. If you set this to {@code false}, users must select one of the + * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown + * if you set this to false and {@link #getChoices} returns {@code null} or empty. + */ + public boolean getAllowFreeFormInput() { + return mAllowFreeFormInput; + } + + /** + * Get additional metadata carried around with this remote input. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Builder class for {@link android.support.v4.app.RemoteInput} objects. + */ + public static final class Builder { + private final String mResultKey; + private CharSequence mLabel; + private CharSequence[] mChoices; + private boolean mAllowFreeFormInput = true; + private Bundle mExtras = new Bundle(); + + /** + * Create a builder object for {@link android.support.v4.app.RemoteInput} objects. + * @param resultKey the Bundle key that refers to this input when collected from the user + */ + public Builder(String resultKey) { + if (resultKey == null) { + throw new IllegalArgumentException("Result key can't be null"); + } + mResultKey = resultKey; + } + + /** + * Set a label to be displayed to the user when collecting this input. + * @param label The label to show to users when they input a response. + * @return this object for method chaining + */ + public Builder setLabel(CharSequence label) { + mLabel = label; + return this; + } + + /** + * Specifies choices available to the user to satisfy this input. + * @param choices an array of pre-defined choices for users input. + * You must provide a non-null and non-empty array if + * you disabled free form input using {@link #setAllowFreeFormInput}. + * @return this object for method chaining + */ + public Builder setChoices(CharSequence[] choices) { + mChoices = choices; + return this; + } + + /** + * Specifies whether the user can provide arbitrary values. + * + * @param allowFreeFormInput The default is {@code true}. + * If you specify {@code false}, you must provide a non-null + * and non-empty array to {@link #setChoices} or an + * {@link IllegalArgumentException} is thrown. + * @return this object for method chaining + */ + public Builder setAllowFreeFormInput(boolean allowFreeFormInput) { + mAllowFreeFormInput = allowFreeFormInput; + return this; + } + + /** + * Merge additional metadata into this builder. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see RemoteInput#getExtras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + *

The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Combine all of the options that have been set and return a new + * {@link android.support.v4.app.RemoteInput} object. + */ + public RemoteInput build() { + return new RemoteInput(mResultKey, mLabel, mChoices, mAllowFreeFormInput, mExtras); + } + } + + /** + * Get the remote input results bundle from an intent. The returned Bundle will + * contain a key/value for every result key populated by remote input collector. + * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. + * @param intent The intent object that fired in response to an action or content intent + * which also had one or more remote input requested. + */ + public static Bundle getResultsFromIntent(Intent intent) { + return IMPL.getResultsFromIntent(intent); + } + + /** + * Populate an intent object with the results gathered from remote input. This method + * should only be called by remote input collection services when sending results to a + * pending intent. + * @param remoteInputs The remote inputs for which results are being provided + * @param intent The intent to add remote inputs to. The {@link android.content.ClipData} + * field of the intent will be modified to contain the results. + * @param results A bundle holding the remote input results. This bundle should + * be populated with keys matching the result keys specified in + * {@code remoteInputs} with values being the result per key. + */ + public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, + Bundle results) { + IMPL.addResultsToIntent(remoteInputs, intent, results); + } + + private static final Impl IMPL; + + interface Impl { + Bundle getResultsFromIntent(Intent intent); + void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, + Bundle results); + } + + static class ImplBase implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + Log.w(TAG, "RemoteInput is only supported from API Level 16"); + return null; + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + Log.w(TAG, "RemoteInput is only supported from API Level 16"); + } + } + + static class ImplJellybean implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + return RemoteInputCompatJellybean.getResultsFromIntent(intent); + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + RemoteInputCompatJellybean.addResultsToIntent(remoteInputs, intent, results); + } + } + + static class ImplApi20 implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + return RemoteInputCompatApi20.getResultsFromIntent(intent); + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + RemoteInputCompatApi20.addResultsToIntent(remoteInputs, intent, results); + } + } + + static { + if (Build.VERSION.SDK_INT >= 20) { + IMPL = new ImplApi20(); + } else if (Build.VERSION.SDK_INT >= 16) { + IMPL = new ImplJellybean(); + } else { + IMPL = new ImplBase(); + } + } + + /** @hide */ + public static final Factory FACTORY = new Factory() { + @Override + public RemoteInput build(String resultKey, + CharSequence label, CharSequence[] choices, boolean allowFreeFormInput, + Bundle extras) { + return new RemoteInput(resultKey, label, choices, allowFreeFormInput, extras); + } + + @Override + public RemoteInput[] newArray(int size) { + return new RemoteInput[size]; + } + }; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java new file mode 100644 index 00000000000..5f302f1fdb3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java @@ -0,0 +1,63 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.RemoteInput; +import android.content.Intent; +import android.os.Bundle; + +class RemoteInputCompatApi20 { + static RemoteInputCompatBase.RemoteInput[] toCompat(RemoteInput[] srcArray, + RemoteInputCompatBase.RemoteInput.Factory factory) { + if (srcArray == null) { + return null; + } + RemoteInputCompatBase.RemoteInput[] result = factory.newArray(srcArray.length); + for (int i = 0; i < srcArray.length; i++) { + RemoteInput src = srcArray[i]; + result[i] = factory.build(src.getResultKey(), src.getLabel(), src.getChoices(), + src.getAllowFreeFormInput(), src.getExtras()); + } + return result; + } + + static RemoteInput[] fromCompat(RemoteInputCompatBase.RemoteInput[] srcArray) { + if (srcArray == null) { + return null; + } + RemoteInput[] result = new RemoteInput[srcArray.length]; + for (int i = 0; i < srcArray.length; i++) { + RemoteInputCompatBase.RemoteInput src = srcArray[i]; + result[i] = new RemoteInput.Builder(src.getResultKey()) + .setLabel(src.getLabel()) + .setChoices(src.getChoices()) + .setAllowFreeFormInput(src.getAllowFreeFormInput()) + .addExtras(src.getExtras()) + .build(); + } + return result; + } + + static Bundle getResultsFromIntent(Intent intent) { + return RemoteInput.getResultsFromIntent(intent); + } + + static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, + Intent intent, Bundle results) { + RemoteInput.addResultsToIntent(fromCompat(remoteInputs), intent, results); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java new file mode 100644 index 00000000000..24493366307 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.os.Bundle; + +class RemoteInputCompatBase { + + public static abstract class RemoteInput { + protected abstract String getResultKey(); + protected abstract CharSequence getLabel(); + protected abstract CharSequence[] getChoices(); + protected abstract boolean getAllowFreeFormInput(); + protected abstract Bundle getExtras(); + + public interface Factory { + public RemoteInput build(String resultKey, CharSequence label, + CharSequence[] choices, boolean allowFreeFormInput, Bundle extras); + public RemoteInput[] newArray(int length); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java new file mode 100644 index 00000000000..36f59819e1f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Intent; +import android.os.Bundle; + +class RemoteInputCompatJellybean { + /** Label used to denote the clip data type used for remote input transport */ + public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results"; + + /** Extra added to a clip data intent object to hold the results bundle. */ + public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; + + private static final String KEY_RESULT_KEY = "resultKey"; + private static final String KEY_LABEL = "label"; + private static final String KEY_CHOICES = "choices"; + private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput"; + private static final String KEY_EXTRAS = "extras"; + + static RemoteInputCompatBase.RemoteInput fromBundle(Bundle data, + RemoteInputCompatBase.RemoteInput.Factory factory) { + return factory.build(data.getString(KEY_RESULT_KEY), + data.getCharSequence(KEY_LABEL), + data.getCharSequenceArray(KEY_CHOICES), + data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT), + data.getBundle(KEY_EXTRAS)); + } + + static Bundle toBundle(RemoteInputCompatBase.RemoteInput remoteInput) { + Bundle data = new Bundle(); + data.putString(KEY_RESULT_KEY, remoteInput.getResultKey()); + data.putCharSequence(KEY_LABEL, remoteInput.getLabel()); + data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices()); + data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput()); + data.putBundle(KEY_EXTRAS, remoteInput.getExtras()); + return data; + } + + static RemoteInputCompatBase.RemoteInput[] fromBundleArray(Bundle[] bundles, + RemoteInputCompatBase.RemoteInput.Factory factory) { + if (bundles == null) { + return null; + } + RemoteInputCompatBase.RemoteInput[] remoteInputs = factory.newArray(bundles.length); + for (int i = 0; i < bundles.length; i++) { + remoteInputs[i] = fromBundle(bundles[i], factory); + } + return remoteInputs; + } + + static Bundle[] toBundleArray(RemoteInputCompatBase.RemoteInput[] remoteInputs) { + if (remoteInputs == null) { + return null; + } + Bundle[] bundles = new Bundle[remoteInputs.length]; + for (int i = 0; i < remoteInputs.length; i++) { + bundles[i] = toBundle(remoteInputs[i]); + } + return bundles; + } + + static Bundle getResultsFromIntent(Intent intent) { + ClipData clipData = intent.getClipData(); + if (clipData == null) { + return null; + } + ClipDescription clipDescription = clipData.getDescription(); + if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { + return null; + } + if (clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) { + return clipData.getItemAt(0).getIntent().getExtras().getParcelable(EXTRA_RESULTS_DATA); + } + return null; + } + + static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, Intent intent, + Bundle results) { + Bundle resultsBundle = new Bundle(); + for (RemoteInputCompatBase.RemoteInput remoteInput : remoteInputs) { + Object result = results.get(remoteInput.getResultKey()); + if (result instanceof CharSequence) { + resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result); + } + } + Intent clipIntent = new Intent(); + clipIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle); + intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipIntent)); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java new file mode 100644 index 00000000000..f644501c49e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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; + +/** + * Helper for accessing features in {@link android.app.Service} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ServiceCompat { + + private ServiceCompat() { + /* Hide constructor */ + } + + /** + * Constant to return from {@link android.app.Service#onStartCommand}: if this + * service's process is killed while it is started (after returning from + * {@link android.app.Service#onStartCommand}), then leave it in the started + * state but don't retain this delivered intent. Later the system will try to + * re-create the service. Because it is in the started state, it will + * guarantee to call {@link android.app.Service#onStartCommand} after creating + * the new service instance; if there are not any pending start commands to be + * delivered to the service, it will be called with a null intent + * object, so you must take care to check for this. + * + *

This mode makes sense for things that will be explicitly started + * and stopped to run for arbitrary periods of time, such as a service + * performing background music playback. + */ + public static final int START_STICKY = 1; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java new file mode 100644 index 00000000000..87ebc492ef6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java @@ -0,0 +1,957 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.StringRes; +import android.support.v4.content.IntentCompat; +import android.support.v4.view.MenuItemCompat; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.ArrayList; + +/** + * Extra helper functionality for sharing data between activities. + * + * ShareCompat provides functionality to extend the {@link Intent#ACTION_SEND}/ + * {@link Intent#ACTION_SEND_MULTIPLE} protocol and support retrieving more info + * about the activity that invoked a social sharing action. + * + * {@link IntentBuilder} provides helper functions for constructing a sharing + * intent that always includes data about the calling activity and app. + * This lets the called activity provide attribution for the app that shared + * content. Constructing an intent this way can be done in a method-chaining style. + * To obtain an IntentBuilder with info about your calling activity, use the static + * method {@link IntentBuilder#from(Activity)}. + * + * {@link IntentReader} provides helper functions for parsing the defined extras + * within an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE} intent + * used to launch an activity. You can also obtain a Drawable for the caller's + * application icon and the application's localized label (the app's human-readable name). + * Social apps that enable sharing content are encouraged to use this information + * to call out the app that the content was shared from. + */ +public class ShareCompat { + /** + * Intent extra that stores the name of the calling package for an ACTION_SEND intent. + * When an activity is started using startActivityForResult this is redundant info. + * (It is also provided by {@link Activity#getCallingPackage()}.) + * + * Instead of using this constant directly, consider using {@link #getCallingPackage(Activity)} + * or {@link IntentReader#getCallingPackage()}. + */ + public static final String EXTRA_CALLING_PACKAGE = + "android.support.v4.app.EXTRA_CALLING_PACKAGE"; + + /** + * Intent extra that stores the {@link ComponentName} of the calling activity for + * an ACTION_SEND intent. + */ + public static final String EXTRA_CALLING_ACTIVITY = + "android.support.v4.app.EXTRA_CALLING_ACTIVITY"; + + /** + * Compatibility shims for sharing operations + */ + interface ShareCompatImpl { + void configureMenuItem(MenuItem item, IntentBuilder shareIntent); + String escapeHtml(CharSequence text); + } + + static class ShareCompatImplBase implements ShareCompatImpl { + public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + item.setIntent(shareIntent.createChooserIntent()); + } + + public String escapeHtml(CharSequence text) { + StringBuilder out = new StringBuilder(); + withinStyle(out, text, 0, text.length()); + return out.toString(); + } + + private static void withinStyle(StringBuilder out, CharSequence text, + int start, int end) { + for (int i = start; i < end; i++) { + char c = text.charAt(i); + + if (c == '<') { + out.append("<"); + } else if (c == '>') { + out.append(">"); + } else if (c == '&') { + out.append("&"); + } else if (c > 0x7E || c < ' ') { + out.append("&#" + ((int) c) + ";"); + } else if (c == ' ') { + while (i + 1 < end && text.charAt(i + 1) == ' ') { + out.append(" "); + i++; + } + + out.append(' '); + } else { + out.append(c); + } + } + } + } + + static class ShareCompatImplICS extends ShareCompatImplBase { + public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + ShareCompatICS.configureMenuItem(item, shareIntent.getActivity(), + shareIntent.getIntent()); + if (shouldAddChooserIntent(item)) { + item.setIntent(shareIntent.createChooserIntent()); + } + } + + boolean shouldAddChooserIntent(MenuItem item) { + return !item.hasSubMenu(); + } + } + + static class ShareCompatImplJB extends ShareCompatImplICS { + public String escapeHtml(CharSequence html) { + return ShareCompatJB.escapeHtml(html); + } + + @Override + boolean shouldAddChooserIntent(MenuItem item) { + return false; + } + } + + private static ShareCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 16) { + IMPL = new ShareCompatImplJB(); + } else if (Build.VERSION.SDK_INT >= 14) { + IMPL = new ShareCompatImplICS(); + } else { + IMPL = new ShareCompatImplBase(); + } + } + + /** + * Retrieve the name of the package that launched calledActivity from a share intent. + * Apps that provide social sharing functionality can use this to provide attribution + * for the app that shared the content. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @param calledActivity Current activity that was launched to share content + * @return Name of the calling package + */ + public static String getCallingPackage(Activity calledActivity) { + String result = calledActivity.getCallingPackage(); + if (result == null) { + result = calledActivity.getIntent().getStringExtra(EXTRA_CALLING_PACKAGE); + } + return result; + } + + /** + * Retrieve the ComponentName of the activity that launched calledActivity from a share intent. + * Apps that provide social sharing functionality can use this to provide attribution + * for the app that shared the content. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @param calledActivity Current activity that was launched to share content + * @return ComponentName of the calling activity + */ + public static ComponentName getCallingActivity(Activity calledActivity) { + ComponentName result = calledActivity.getCallingActivity(); + if (result == null) { + result = calledActivity.getIntent().getParcelableExtra(EXTRA_CALLING_ACTIVITY); + } + return result; + } + + /** + * Configure a {@link MenuItem} to act as a sharing action. + * + *

If the app is running on API level 14 or higher (Android 4.0/Ice Cream Sandwich) + * this method will configure a ShareActionProvider to provide a more robust UI + * for selecting the target of the share. History will be tracked for each calling + * activity in a file named with the prefix ".sharecompat_" in the application's + * private data directory. If the application wishes to set this MenuItem to show + * as an action in the Action Bar it should use + * {@link MenuItemCompat#setShowAsAction(MenuItem, int)} to request that behavior + * in addition to calling this method.

+ * + *

If the app is running on an older platform version this method will configure + * a standard activity chooser dialog for the menu item.

+ * + *

During the calling activity's lifecycle, if data within the share intent must + * change the app should change that state in one of several ways:

+ *
    + *
  • Call {@link ActivityCompat#invalidateOptionsMenu(Activity)}. If the app is running + * on API level 11 or above and uses the Action Bar its menu will be recreated and rebuilt. + * If not, the activity will receive a call to {@link Activity#onPrepareOptionsMenu(Menu)} + * the next time the user presses the menu key to open the options menu panel. The activity + * can then call configureMenuItem again with a new or altered IntentBuilder to reconfigure + * the share menu item.
  • + *
  • Keep a reference to the MenuItem object for the share item once it has been created + * and call configureMenuItem to update the associated sharing intent as needed.
  • + *
+ * + * @param item MenuItem to configure for sharing + * @param shareIntent IntentBuilder with data about the content to share + */ + public static void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + IMPL.configureMenuItem(item, shareIntent); + } + + /** + * Configure a menu item to act as a sharing action. + * + * @param menu Menu containing the item to use for sharing + * @param menuItemId ID of the share item within menu + * @param shareIntent IntentBuilder with data about the content to share + * @see #configureMenuItem(MenuItem, IntentBuilder) + */ + public static void configureMenuItem(Menu menu, int menuItemId, IntentBuilder shareIntent) { + MenuItem item = menu.findItem(menuItemId); + if (item == null) { + throw new IllegalArgumentException("Could not find menu item with id " + menuItemId + + " in the supplied menu"); + } + configureMenuItem(item, shareIntent); + } + + /** + * IntentBuilder is a helper for constructing {@link Intent#ACTION_SEND} and + * {@link Intent#ACTION_SEND_MULTIPLE} sharing intents and starting activities + * to share content. The ComponentName and package name of the calling activity + * will be included. + */ + public static class IntentBuilder { + private Activity mActivity; + private Intent mIntent; + private CharSequence mChooserTitle; + private ArrayList mToAddresses; + private ArrayList mCcAddresses; + private ArrayList mBccAddresses; + + private ArrayList mStreams; + + /** + * Create a new IntentBuilder for launching a sharing action from launchingActivity. + * + * @param launchingActivity Activity that the share will be launched from + * @return a new IntentBuilder instance + */ + public static IntentBuilder from(Activity launchingActivity) { + return new IntentBuilder(launchingActivity); + } + + private IntentBuilder(Activity launchingActivity) { + mActivity = launchingActivity; + mIntent = new Intent().setAction(Intent.ACTION_SEND); + mIntent.putExtra(EXTRA_CALLING_PACKAGE, launchingActivity.getPackageName()); + mIntent.putExtra(EXTRA_CALLING_ACTIVITY, launchingActivity.getComponentName()); + mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + } + + /** + * Retrieve the Intent as configured so far by the IntentBuilder. This Intent + * is suitable for use in a ShareActionProvider or chooser dialog. + * + *

To create an intent that will launch the activity chooser so that the user + * may select a target for the share, see {@link #createChooserIntent()}. + * + * @return The current Intent being configured by this builder + */ + public Intent getIntent() { + if (mToAddresses != null) { + combineArrayExtra(Intent.EXTRA_EMAIL, mToAddresses); + mToAddresses = null; + } + if (mCcAddresses != null) { + combineArrayExtra(Intent.EXTRA_CC, mCcAddresses); + mCcAddresses = null; + } + if (mBccAddresses != null) { + combineArrayExtra(Intent.EXTRA_BCC, mBccAddresses); + mBccAddresses = null; + } + + // Check if we need to change the action. + boolean needsSendMultiple = mStreams != null && mStreams.size() > 1; + boolean isSendMultiple = mIntent.getAction().equals(Intent.ACTION_SEND_MULTIPLE); + + if (!needsSendMultiple && isSendMultiple) { + // Change back to a single send action; place the first stream into the + // intent for single sharing. + mIntent.setAction(Intent.ACTION_SEND); + if (mStreams != null && !mStreams.isEmpty()) { + mIntent.putExtra(Intent.EXTRA_STREAM, mStreams.get(0)); + } else { + mIntent.removeExtra(Intent.EXTRA_STREAM); + } + mStreams = null; + } + + if (needsSendMultiple && !isSendMultiple) { + // Change to a multiple send action; place the relevant ArrayList into the + // intent for multiple sharing. + mIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + if (mStreams != null && !mStreams.isEmpty()) { + mIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mStreams); + } else { + mIntent.removeExtra(Intent.EXTRA_STREAM); + } + } + + return mIntent; + } + + Activity getActivity() { + return mActivity; + } + + private void combineArrayExtra(String extra, ArrayList add) { + String[] currentAddresses = mIntent.getStringArrayExtra(extra); + int currentLength = currentAddresses != null ? currentAddresses.length : 0; + String[] finalAddresses = new String[currentLength + add.size()]; + add.toArray(finalAddresses); + if (currentAddresses != null) { + System.arraycopy(currentAddresses, 0, finalAddresses, add.size(), currentLength); + } + mIntent.putExtra(extra, finalAddresses); + } + + private void combineArrayExtra(String extra, String[] add) { + // Add any items still pending + Intent intent = getIntent(); + String[] old = intent.getStringArrayExtra(extra); + int oldLength = old != null ? old.length : 0; + String[] result = new String[oldLength + add.length]; + if (old != null) System.arraycopy(old, 0, result, 0, oldLength); + System.arraycopy(add, 0, result, oldLength, add.length); + intent.putExtra(extra, result); + } + + /** + * Create an Intent that will launch the standard Android activity chooser, + * allowing the user to pick what activity/app on the system should handle + * the share. + * + * @return A chooser Intent for the currently configured sharing action + */ + public Intent createChooserIntent() { + return Intent.createChooser(getIntent(), mChooserTitle); + } + + /** + * Start a chooser activity for the current share intent. + * + *

Note that under most circumstances you should use + * {@link ShareCompat#configureMenuItem(MenuItem, IntentBuilder) + * ShareCompat.configureMenuItem()} to add a Share item to the menu while + * presenting a detail view of the content to be shared instead + * of invoking this directly.

+ */ + public void startChooser() { + mActivity.startActivity(createChooserIntent()); + } + + /** + * Set the title that will be used for the activity chooser for this share. + * + * @param title Title string + * @return This IntentBuilder for method chaining + */ + public IntentBuilder setChooserTitle(CharSequence title) { + mChooserTitle = title; + return this; + } + + /** + * Set the title that will be used for the activity chooser for this share. + * + * @param resId Resource ID of the title string to use + * @return This IntentBuilder for method chaining + */ + public IntentBuilder setChooserTitle(@StringRes int resId) { + return setChooserTitle(mActivity.getText(resId)); + } + + /** + * Set the type of data being shared + * + * @param mimeType mimetype of the shared data + * @return This IntentBuilder for method chaining + * @see Intent#setType(String) + */ + public IntentBuilder setType(String mimeType) { + mIntent.setType(mimeType); + return this; + } + + /** + * Set the literal text data to be sent as part of the share. + * This may be a styled CharSequence. + * + * @param text Text to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_TEXT + */ + public IntentBuilder setText(CharSequence text) { + mIntent.putExtra(Intent.EXTRA_TEXT, text); + return this; + } + + /** + * Set an HTML string to be sent as part of the share. + * If {@link Intent#EXTRA_TEXT EXTRA_TEXT} has not already been supplied, + * a styled version of the supplied HTML text will be added as EXTRA_TEXT as + * parsed by {@link android.text.Html#fromHtml(String) Html.fromHtml}. + * + * @param htmlText A string containing HTML markup as a richer version of the text + * provided by EXTRA_TEXT. + * @return This IntentBuilder for method chaining + * @see #setText(CharSequence) + */ + public IntentBuilder setHtmlText(String htmlText) { + mIntent.putExtra(IntentCompat.EXTRA_HTML_TEXT, htmlText); + if (!mIntent.hasExtra(Intent.EXTRA_TEXT)) { + // Supply a default if EXTRA_TEXT isn't set + setText(Html.fromHtml(htmlText)); + } + return this; + } + + /** + * Set a stream URI to the data that should be shared. + * + *

This replaces all currently set stream URIs and will produce a single-stream + * ACTION_SEND intent.

+ * + * @param streamUri URI of the stream to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_STREAM + */ + public IntentBuilder setStream(Uri streamUri) { + if (!mIntent.getAction().equals(Intent.ACTION_SEND)) { + mIntent.setAction(Intent.ACTION_SEND); + } + mStreams = null; + mIntent.putExtra(Intent.EXTRA_STREAM, streamUri); + return this; + } + + /** + * Add a stream URI to the data that should be shared. If this is not the first + * stream URI added the final intent constructed will become an ACTION_SEND_MULTIPLE + * intent. Not all apps will handle both ACTION_SEND and ACTION_SEND_MULTIPLE. + * + * @param streamUri URI of the stream to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_STREAM + * @see Intent#ACTION_SEND + * @see Intent#ACTION_SEND_MULTIPLE + */ + public IntentBuilder addStream(Uri streamUri) { + Uri currentStream = mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + if (currentStream == null) { + return setStream(streamUri); + } + if (mStreams == null) { + mStreams = new ArrayList(); + } + if (currentStream != null) { + mIntent.removeExtra(Intent.EXTRA_STREAM); + mStreams.add(currentStream); + } + mStreams.add(streamUri); + return this; + } + + /** + * Set an array of email addresses as recipients of this share. + * This replaces all current "to" recipients that have been set so far. + * + * @param addresses Email addresses to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder setEmailTo(String[] addresses) { + if (mToAddresses != null) { + mToAddresses = null; + } + mIntent.putExtra(Intent.EXTRA_EMAIL, addresses); + return this; + } + + /** + * Add an email address to be used in the "to" field of the final Intent. + * + * @param address Email address to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder addEmailTo(String address) { + if (mToAddresses == null) { + mToAddresses = new ArrayList(); + } + mToAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "to" field of the final Intent. + * + * @param addresses Email addresses to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder addEmailTo(String[] addresses) { + combineArrayExtra(Intent.EXTRA_EMAIL, addresses); + return this; + } + + /** + * Set an array of email addresses to CC on this share. + * This replaces all current "CC" recipients that have been set so far. + * + * @param addresses Email addresses to CC on the share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder setEmailCc(String[] addresses) { + mIntent.putExtra(Intent.EXTRA_CC, addresses); + return this; + } + + /** + * Add an email address to be used in the "cc" field of the final Intent. + * + * @param address Email address to CC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder addEmailCc(String address) { + if (mCcAddresses == null) { + mCcAddresses = new ArrayList(); + } + mCcAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "cc" field of the final Intent. + * + * @param addresses Email addresses to CC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder addEmailCc(String[] addresses) { + combineArrayExtra(Intent.EXTRA_CC, addresses); + return this; + } + + /** + * Set an array of email addresses to BCC on this share. + * This replaces all current "BCC" recipients that have been set so far. + * + * @param addresses Email addresses to BCC on the share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder setEmailBcc(String[] addresses) { + mIntent.putExtra(Intent.EXTRA_BCC, addresses); + return this; + } + + /** + * Add an email address to be used in the "bcc" field of the final Intent. + * + * @param address Email address to BCC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder addEmailBcc(String address) { + if (mBccAddresses == null) { + mBccAddresses = new ArrayList(); + } + mBccAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "bcc" field of the final Intent. + * + * @param addresses Email addresses to BCC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder addEmailBcc(String[] addresses) { + combineArrayExtra(Intent.EXTRA_BCC, addresses); + return this; + } + + /** + * Set a subject heading for this share; useful for sharing via email. + * + * @param subject Subject heading for this share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_SUBJECT + */ + public IntentBuilder setSubject(String subject) { + mIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + return this; + } + } + + /** + * IntentReader is a helper for reading the data contained within a sharing (ACTION_SEND) + * Intent. It provides methods to parse standard elements included with a share + * in addition to extra metadata about the app that shared the content. + * + *

Social sharing apps are encouraged to provide attribution for the app that shared + * the content. IntentReader offers access to the application label, calling activity info, + * and application icon of the app that shared the content. This data may have been provided + * voluntarily by the calling app and should always be displayed to the user before submission + * for manual verification. The user should be offered the option to omit this information + * from shared posts if desired.

+ * + *

Activities that intend to receive sharing intents should configure an intent-filter + * to accept {@link Intent#ACTION_SEND} intents ("android.intent.action.SEND") and optionally + * accept {@link Intent#ACTION_SEND_MULTIPLE} ("android.intent.action.SEND_MULTIPLE") if + * the activity is equipped to handle multiple data streams.

+ */ + public static class IntentReader { + private static final String TAG = "IntentReader"; + + private Activity mActivity; + private Intent mIntent; + private String mCallingPackage; + private ComponentName mCallingActivity; + + private ArrayList mStreams; + + /** + * Get an IntentReader for parsing and interpreting the sharing intent + * used to start the given activity. + * + * @param activity Activity that was started to share content + * @return IntentReader for parsing sharing data + */ + public static IntentReader from(Activity activity) { + return new IntentReader(activity); + } + + private IntentReader(Activity activity) { + mActivity = activity; + mIntent = activity.getIntent(); + mCallingPackage = ShareCompat.getCallingPackage(activity); + mCallingActivity = ShareCompat.getCallingActivity(activity); + } + + /** + * Returns true if the activity this reader was obtained for was + * started with an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE} + * sharing Intent. + * + * @return true if the activity was started with an ACTION_SEND + * or ACTION_SEND_MULTIPLE Intent + */ + public boolean isShareIntent() { + final String action = mIntent.getAction(); + return Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action); + } + + /** + * Returns true if the activity this reader was obtained for was started with an + * {@link Intent#ACTION_SEND} intent and contains a single shared item. + * The shared content should be obtained using either the {@link #getText()} + * or {@link #getStream()} methods depending on the type of content shared. + * + * @return true if the activity was started with an ACTION_SEND intent + */ + public boolean isSingleShare() { + return Intent.ACTION_SEND.equals(mIntent.getAction()); + } + + /** + * Returns true if the activity this reader was obtained for was started with an + * {@link Intent#ACTION_SEND_MULTIPLE} intent. The Intent may contain more than + * one stream item. + * + * @return true if the activity was started with an ACTION_SEND_MULTIPLE intent + */ + public boolean isMultipleShare() { + return Intent.ACTION_SEND_MULTIPLE.equals(mIntent.getAction()); + } + + /** + * Get the mimetype of the data shared to this activity. + * + * @return mimetype of the shared data + * @see Intent#getType() + */ + public String getType() { + return mIntent.getType(); + } + + /** + * Get the literal text shared with the target activity. + * + * @return Literal shared text or null if none was supplied + * @see Intent#EXTRA_TEXT + */ + public CharSequence getText() { + return mIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); + } + + /** + * Get the styled HTML text shared with the target activity. + * If no HTML text was supplied but {@link Intent#EXTRA_TEXT} contained + * styled text, it will be converted to HTML if possible and returned. + * If the text provided by {@link Intent#EXTRA_TEXT} was not styled text, + * it will be escaped by {@link android.text.Html#escapeHtml(CharSequence)} + * and returned. If no text was provided at all, this method will return null. + * + * @return Styled text provided by the sender as HTML. + */ + public String getHtmlText() { + String result = mIntent.getStringExtra(IntentCompat.EXTRA_HTML_TEXT); + if (result == null) { + CharSequence text = getText(); + if (text instanceof Spanned) { + result = Html.toHtml((Spanned) text); + } else if (text != null) { + result = IMPL.escapeHtml(text); + } + } + return result; + } + + /** + * Get a URI referring to a data stream shared with the target activity. + * + *

This call will fail if the share intent contains multiple stream items. + * If {@link #isMultipleShare()} returns true the application should use + * {@link #getStream(int)} and {@link #getStreamCount()} to retrieve the + * included stream items.

+ * + * @return A URI referring to a data stream to be shared or null if one was not supplied + * @see Intent#EXTRA_STREAM + */ + public Uri getStream() { + return mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + } + + /** + * Get the URI of a stream item shared with the target activity. + * Index should be in the range [0-getStreamCount()). + * + * @param index Index of text item to retrieve + * @return Requested stream item URI + * @see Intent#EXTRA_STREAM + * @see Intent#ACTION_SEND_MULTIPLE + */ + public Uri getStream(int index) { + if (mStreams == null && isMultipleShare()) { + mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } + if (mStreams != null) { + return mStreams.get(index); + } + if (index == 0) { + return mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + } + throw new IndexOutOfBoundsException("Stream items available: " + getStreamCount() + + " index requested: " + index); + } + + /** + * Return the number of stream items shared. The return value will be 0 or 1 if + * this was an {@link Intent#ACTION_SEND} intent, or 0 or more if it was an + * {@link Intent#ACTION_SEND_MULTIPLE} intent. + * + * @return Count of text items contained within the Intent + */ + public int getStreamCount() { + if (mStreams == null && isMultipleShare()) { + mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } + if (mStreams != null) { + return mStreams.size(); + } + return mIntent.hasExtra(Intent.EXTRA_STREAM) ? 1 : 0; + } + + /** + * Get an array of Strings, each an email address to share to. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_EMAIL + */ + public String[] getEmailTo() { + return mIntent.getStringArrayExtra(Intent.EXTRA_EMAIL); + } + + /** + * Get an array of Strings, each an email address to CC on this share. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_CC + */ + public String[] getEmailCc() { + return mIntent.getStringArrayExtra(Intent.EXTRA_CC); + } + + /** + * Get an array of Strings, each an email address to BCC on this share. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_BCC + */ + public String[] getEmailBcc() { + return mIntent.getStringArrayExtra(Intent.EXTRA_BCC); + } + + /** + * Get a subject heading for this share; useful when sharing via email. + * + * @return The subject heading for this share or null if one was not supplied. + * @see Intent#EXTRA_SUBJECT + */ + public String getSubject() { + return mIntent.getStringExtra(Intent.EXTRA_SUBJECT); + } + + /** + * Get the name of the package that invoked this sharing intent. If the activity + * was not started for a result, IntentBuilder will read this from extra metadata placed + * in the Intent by ShareBuilder. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return Name of the package that started this activity or null if unknown + * @see Activity#getCallingPackage() + * @see ShareCompat#EXTRA_CALLING_PACKAGE + */ + public String getCallingPackage() { + return mCallingPackage; + } + + /** + * Get the {@link ComponentName} of the Activity that invoked this sharing intent. + * If the target sharing activity was not started for a result, IntentBuilder will read + * this from extra metadata placed in the intent by ShareBuilder. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return ComponentName of the calling Activity or null if unknown + * @see Activity#getCallingActivity() + * @see ShareCompat#EXTRA_CALLING_ACTIVITY + */ + public ComponentName getCallingActivity() { + return mCallingActivity; + } + + /** + * Get the icon of the calling activity as a Drawable if data about + * the calling activity is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling Activity's icon or null if unknown + */ + public Drawable getCallingActivityIcon() { + if (mCallingActivity == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getActivityIcon(mCallingActivity); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve icon for calling activity", e); + } + return null; + } + + /** + * Get the icon of the calling application as a Drawable if data + * about the calling package is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling application's icon or null if unknown + */ + public Drawable getCallingApplicationIcon() { + if (mCallingPackage == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getApplicationIcon(mCallingPackage); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve icon for calling application", e); + } + return null; + } + + /** + * Get the human-readable label (title) of the calling application if + * data about the calling package is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling application's label or null if unknown + */ + public CharSequence getCallingApplicationLabel() { + if (mCallingPackage == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getApplicationLabel(pm.getApplicationInfo(mCallingPackage, 0)); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve label for calling application", e); + } + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java new file mode 100644 index 00000000000..0f2ff352bc8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.app.Activity; +import android.content.Intent; +import android.view.ActionProvider; +import android.view.MenuItem; +import android.widget.ShareActionProvider; + +class ShareCompatICS { + private static final String HISTORY_FILENAME_PREFIX = ".sharecompat_"; + + public static void configureMenuItem(MenuItem item, Activity callingActivity, Intent intent) { + ActionProvider itemProvider = item.getActionProvider(); + ShareActionProvider provider = null; + if (!(itemProvider instanceof ShareActionProvider)) { + provider = new ShareActionProvider(callingActivity); + } else { + provider = (ShareActionProvider) itemProvider; + } + provider.setShareHistoryFileName(HISTORY_FILENAME_PREFIX + + callingActivity.getClass().getName()); + provider.setShareIntent(intent); + item.setActionProvider(provider); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java new file mode 100644 index 00000000000..8a4de97af8f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.text.Html; + +class ShareCompatJB { + public static String escapeHtml(CharSequence html) { + return Html.escapeHtml(html); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java b/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java new file mode 100644 index 00000000000..7c0de86d5a7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java @@ -0,0 +1,264 @@ +/* + * 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. + */ +package android.support.v4.app; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import java.util.List; +import java.util.Map; + +/** + * Listener provided in + * {@link FragmentActivity#setEnterSharedElementCallback(SharedElementCallback)} and + * {@link FragmentActivity#setExitSharedElementCallback(SharedElementCallback)} + * to monitor the Activity transitions. The events can be used to customize Activity + * Transition behavior. + */ +public abstract class SharedElementCallback { + private Matrix mTempMatrix; + private static int MAX_IMAGE_SIZE = (1024 * 1024); + private static final String BUNDLE_SNAPSHOT_BITMAP = "sharedElement:snapshot:bitmap"; + private static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "sharedElement:snapshot:imageScaleType"; + private static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "sharedElement:snapshot:imageMatrix"; + + /** + * Called immediately after the start state is set for the shared element. + * The shared element will start at the size and position of the shared element + * in the launching Activity or Fragment. + * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. This list is null for Fragment + * Transitions. + */ + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) {} + + /** + * Called after the end state is set for the shared element, but before the end state + * is captured by the shared element transition. + *

+ * Any customization done in + * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} + * may need to be modified to the final state of the shared element if it is not + * automatically corrected by layout. For example, rotation or scale will not + * be affected by layout and if changed in {@link #onSharedElementStart(java.util.List, + * java.util.List, java.util.List)}, it will also have to be set here again to correct + * the end state. + *

+ * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. This list will be null for + * Fragment Transitions. + */ + public void onSharedElementEnd(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) {} + + /** + * Called after {@link #onMapSharedElements(java.util.List, java.util.Map)} when + * transferring shared elements in. Any shared elements that have no mapping will be in + * rejectedSharedElements. The elements remaining in + * rejectedSharedElements will be transitioned out of the Scene. If a + * View is removed from rejectedSharedElements, it must be handled by the + * SharedElementListener. + *

+ * Views in rejectedSharedElements will have their position and size set to the + * position of the calling shared element, relative to the Window decor View and contain + * snapshots of the View from the calling Activity or Fragment. This + * view may be safely added to the decor View's overlay to remain in position. + *

+ *

This method is not called for Fragment Transitions. All rejected shared elements + * will be handled by the exit transition.

+ * + * @param rejectedSharedElements Views containing visual information of shared elements + * that are not part of the entering scene. These Views + * are positioned relative to the Window decor View. A + * View removed from this list will not be transitioned + * automatically. + */ + public void onRejectSharedElements(List rejectedSharedElements) {} + + /** + * Lets the SharedElementCallback adjust the mapping of shared element names to + * Views. + * + * @param names The names of all shared elements transferred from the calling Activity + * or Fragment in the order they were provided. + * @param sharedElements The mapping of shared element names to Views. The best guess + * will be filled into sharedElements based on the transitionNames. + */ + public void onMapSharedElements(List names, Map sharedElements) {} + + + /** + * Creates a snapshot of a shared element to be used by the remote Activity and reconstituted + * with {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)}. A + * null return value will mean that the remote Activity will have a null snapshot View in + * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and + * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}. + * + *

This is not called for Fragment Transitions.

+ * + * @param sharedElement The shared element View to create a snapshot for. + * @param viewToGlobalMatrix A matrix containing a transform from the view to the screen + * coordinates. + * @param screenBounds The bounds of shared element in screen coordinate space. This is + * the bounds of the view with the viewToGlobalMatrix applied. + * @return A snapshot to send to the remote Activity to be reconstituted with + * {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)} and passed + * into {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and + * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}. + */ + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, + RectF screenBounds) { + if (sharedElement instanceof ImageView) { + ImageView imageView = ((ImageView) sharedElement); + Drawable d = imageView.getDrawable(); + Drawable bg = imageView.getBackground(); + if (d != null && bg == null) { + Bitmap bitmap = createDrawableBitmap(d); + if (bitmap != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap); + bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE, + imageView.getScaleType().toString()); + if (imageView.getScaleType() == ScaleType.MATRIX) { + Matrix matrix = imageView.getImageMatrix(); + float[] values = new float[9]; + matrix.getValues(values); + bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values); + } + return bundle; + } + } + } + int bitmapWidth = Math.round(screenBounds.width()); + int bitmapHeight = Math.round(screenBounds.height()); + Bitmap bitmap = null; + if (bitmapWidth > 0 && bitmapHeight > 0) { + float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight)); + bitmapWidth *= scale; + bitmapHeight *= scale; + if (mTempMatrix == null) { + mTempMatrix = new Matrix(); + } + mTempMatrix.set(viewToGlobalMatrix); + mTempMatrix.postTranslate(-screenBounds.left, -screenBounds.top); + mTempMatrix.postScale(scale, scale); + bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.concat(mTempMatrix); + sharedElement.draw(canvas); + } + return bitmap; + } + + /** + * Get a copy of bitmap of given drawable. + */ + private static Bitmap createDrawableBitmap(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + if (width <= 0 || height <= 0) { + return null; + } + float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (width * height)); + if (drawable instanceof BitmapDrawable && scale == 1f) { + // return same bitmap if scale down not needed + return ((BitmapDrawable) drawable).getBitmap(); + } + int bitmapWidth = (int) (width * scale); + int bitmapHeight = (int) (height * scale); + Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Rect existingBounds = drawable.getBounds(); + int left = existingBounds.left; + int top = existingBounds.top; + int right = existingBounds.right; + int bottom = existingBounds.bottom; + drawable.setBounds(0, 0, bitmapWidth, bitmapHeight); + drawable.draw(canvas); + drawable.setBounds(left, top, right, bottom); + return bitmap; + } + + /** + * Reconstitutes a snapshot View from a Parcelable returned in + * {@link #onCaptureSharedElementSnapshot(android.view.View, android.graphics.Matrix, + * android.graphics.RectF)} to be used in {@link #onSharedElementStart(java.util.List, + * java.util.List, java.util.List)} and {@link #onSharedElementEnd(java.util.List, + * java.util.List, java.util.List)}. The returned View will be sized and positioned after + * this call so that it is ready to be added to the decor View's overlay. + * + *

This is not called for Fragment Transitions.

+ * + * @param context The Context used to create the snapshot View. + * @param snapshot The Parcelable returned by {@link #onCaptureSharedElementSnapshot( + * android.view.View, android.graphics.Matrix, android.graphics.RectF)}. + * @return A View to be sent in {@link #onSharedElementStart(java.util.List, java.util.List, + * java.util.List)} and {@link #onSharedElementEnd(java.util.List, java.util.List, + * java.util.List)}. A null value will produce a null snapshot value for those two methods. + */ + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + ImageView view = null; + if (snapshot instanceof Bundle) { + Bundle bundle = (Bundle) snapshot; + Bitmap bitmap = (Bitmap) bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP); + if (bitmap == null) { + return null; + } + ImageView imageView = new ImageView(context); + view = imageView; + imageView.setImageBitmap(bitmap); + imageView.setScaleType( + ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE))); + if (imageView.getScaleType() == ScaleType.MATRIX) { + float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX); + Matrix matrix = new Matrix(); + matrix.setValues(values); + imageView.setImageMatrix(matrix); + } + } else if (snapshot instanceof Bitmap) { + Bitmap bitmap = (Bitmap) snapshot; + view = new ImageView(context); + view.setImageBitmap(bitmap); + } + return view; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java b/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java new file mode 100644 index 00000000000..0313eb996d6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util.AndroidRuntimeException; + +final class SuperNotCalledException extends AndroidRuntimeException { + public SuperNotCalledException(String msg) { + super(msg); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java new file mode 100644 index 00000000000..9146872cd97 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java @@ -0,0 +1,393 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.IntentCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Utility class for constructing synthetic back stacks for cross-task navigation + * on Android 3.0 and newer. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

TaskStackBuilder provides a backward-compatible way to obey the correct conventions + * around cross-task navigation on the device's version of the platform. On devices running + * Android 3.0 or newer, calls to the {@link #startActivities()} method or sending the + * {@link PendingIntent} generated by {@link #getPendingIntent(int, int)} will construct + * the synthetic back stack as prescribed. On devices running older versions of the platform, + * these same calls will invoke the topmost activity in the supplied stack, ignoring + * the rest of the synthetic stack and allowing the back key to navigate back to the previous + * task.

+ * + *
+ *

About Navigation

+ * For more detailed information about tasks, the back stack, and navigation design guidelines, + * please read + * Tasks and Back Stack + * from the developer guide and Navigation + * from the design guide. + *
+ */ +public class TaskStackBuilder implements Iterable { + private static final String TAG = "TaskStackBuilder"; + + public interface SupportParentable { + Intent getSupportParentActivityIntent(); + } + + interface TaskStackBuilderImpl { + PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options); + } + + static class TaskStackBuilderImplBase implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + Intent topIntent = new Intent(intents[intents.length - 1]); + topIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(context, requestCode, topIntent, flags); + } + } + + static class TaskStackBuilderImplHoneycomb implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + return TaskStackBuilderHoneycomb.getActivitiesPendingIntent(context, requestCode, + intents, flags); + } + } + + static class TaskStackBuilderImplJellybean implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + return TaskStackBuilderJellybean.getActivitiesPendingIntent(context, requestCode, + intents, flags, options); + } + } + + private static final TaskStackBuilderImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 11) { + IMPL = new TaskStackBuilderImplHoneycomb(); + } else { + IMPL = new TaskStackBuilderImplBase(); + } + } + + private final ArrayList mIntents = new ArrayList(); + private final Context mSourceContext; + + private TaskStackBuilder(Context a) { + mSourceContext = a; + } + + /** + * Return a new TaskStackBuilder for launching a fresh task stack consisting + * of a series of activities. + * + * @param context The context that will launch the new task stack or generate a PendingIntent + * @return A new TaskStackBuilder + */ + public static TaskStackBuilder create(Context context) { + return new TaskStackBuilder(context); + } + + /** + * Return a new TaskStackBuilder for launching a fresh task stack consisting + * of a series of activities. + * + * @param context The context that will launch the new task stack or generate a PendingIntent + * @return A new TaskStackBuilder + * + * @deprecated use {@link #create(Context)} instead + */ + public static TaskStackBuilder from(Context context) { + return create(context); + } + + /** + * Add a new Intent to the task stack. The most recently added Intent will invoke + * the Activity at the top of the final task stack. + * + * @param nextIntent Intent for the next Activity in the synthesized task stack + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addNextIntent(Intent nextIntent) { + mIntents.add(nextIntent); + return this; + } + + /** + * Add a new Intent with the resolved chain of parents for the target activity to + * the task stack. + * + *

This is equivalent to calling {@link #addParentStack(ComponentName) addParentStack} + * with the resolved ComponentName of nextIntent (if it can be resolved), followed by + * {@link #addNextIntent(Intent) addNextIntent} with nextIntent.

+ * + * @param nextIntent Intent for the topmost Activity in the synthesized task stack. + * Its chain of parents as specified in the manifest will be added. + * @return This TaskStackBuilder for method chaining. + */ + public TaskStackBuilder addNextIntentWithParentStack(Intent nextIntent) { + ComponentName target = nextIntent.getComponent(); + if (target == null) { + target = nextIntent.resolveActivity(mSourceContext.getPackageManager()); + } + if (target != null) { + addParentStack(target); + } + addNextIntent(nextIntent); + return this; + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivity All parents of this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(Activity sourceActivity) { + Intent parent = null; + if (sourceActivity instanceof SupportParentable) { + parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent(); + } + if (parent == null) { + parent = NavUtils.getParentActivityIntent(sourceActivity); + } + + if (parent != null) { + // We have the actual parent intent, build the rest from static metadata + // then add the direct parent intent to the end. + ComponentName target = parent.getComponent(); + if (target == null) { + target = parent.resolveActivity(mSourceContext.getPackageManager()); + } + addParentStack(target); + addNextIntent(parent); + } + return this; + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivityClass All parents of this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(Class sourceActivityClass) { + return addParentStack(new ComponentName(mSourceContext, sourceActivityClass)); + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivityName Must specify an Activity component. All parents of + * this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(ComponentName sourceActivityName) { + final int insertAt = mIntents.size(); + try { + Intent parent = NavUtils.getParentActivityIntent(mSourceContext, sourceActivityName); + while (parent != null) { + mIntents.add(insertAt, parent); + parent = NavUtils.getParentActivityIntent(mSourceContext, parent.getComponent()); + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Bad ComponentName while traversing activity parent metadata"); + throw new IllegalArgumentException(e); + } + return this; + } + + /** + * @return the number of intents added so far. + */ + public int getIntentCount() { + return mIntents.size(); + } + + /** + * Get the intent at the specified index. + * Useful if you need to modify the flags or extras of an intent that was previously added, + * for example with {@link #addParentStack(Activity)}. + * + * @param index Index from 0-getIntentCount() + * @return the intent at position index + * + * @deprecated Renamed to editIntentAt to better reflect intended usage + */ + public Intent getIntent(int index) { + return editIntentAt(index); + } + + /** + * Return the intent at the specified index for modification. + * Useful if you need to modify the flags or extras of an intent that was previously added, + * for example with {@link #addParentStack(Activity)}. + * + * @param index Index from 0-getIntentCount() + * @return the intent at position index + */ + public Intent editIntentAt(int index) { + return mIntents.get(index); + } + + /** + * @deprecated Use editIntentAt instead + */ + public Iterator iterator() { + return mIntents.iterator(); + } + + /** + * Start the task stack constructed by this builder. The Context used to obtain + * this builder must be an Activity. + * + *

On devices that do not support API level 11 or higher the topmost activity + * will be started as a new task. On devices that do support API level 11 or higher + * the new task stack will be created in its entirety.

+ */ + public void startActivities() { + startActivities(null); + } + + /** + * Start the task stack constructed by this builder. The Context used to obtain + * this builder must be an Activity. + * + *

On devices that do not support API level 11 or higher the topmost activity + * will be started as a new task. On devices that do support API level 11 or higher + * the new task stack will be created in its entirety.

+ * + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + */ + public void startActivities(Bundle options) { + if (mIntents.isEmpty()) { + throw new IllegalStateException( + "No intents added to TaskStackBuilder; cannot startActivities"); + } + + Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]); + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + if (!ContextCompat.startActivities(mSourceContext, intents, options)) { + Intent topIntent = new Intent(intents[intents.length - 1]); + topIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mSourceContext.startActivity(topIntent); + } + } + + /** + * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far. + * + * @param requestCode Private request code for the sender + * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT}, + * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT}, + * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by + * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the + * intent that can be supplied when the actual send happens. + * @return The obtained PendingIntent + */ + public PendingIntent getPendingIntent(int requestCode, int flags) { + return getPendingIntent(requestCode, flags, null); + } + + /** + * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far. + * + * @param requestCode Private request code for the sender + * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT}, + * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT}, + * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by + * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the + * intent that can be supplied when the actual send happens. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * @return The obtained PendingIntent + */ + public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) { + if (mIntents.isEmpty()) { + throw new IllegalStateException( + "No intents added to TaskStackBuilder; cannot getPendingIntent"); + } + + Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]); + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + // Appropriate flags will be added by the call below. + return IMPL.getPendingIntent(mSourceContext, intents, requestCode, flags, options); + } + + /** + * Return an array containing the intents added to this builder. The intent at the + * root of the task stack will appear as the first item in the array and the + * intent at the top of the stack will appear as the last item. + * + * @return An array containing the intents added to this builder. + */ + public Intent[] getIntents() { + Intent[] intents = new Intent[mIntents.size()]; + if (intents.length == 0) return intents; + + intents[0] = new Intent(mIntents.get(0)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + for (int i = 1; i < intents.length; i++) { + intents[i] = new Intent(mIntents.get(i)); + } + return intents; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java new file mode 100644 index 00000000000..8bb602e4b69 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +/** + * Implementation of TaskStackBuilder that can call Honeycomb APIs. + */ +class TaskStackBuilderHoneycomb { + public static PendingIntent getActivitiesPendingIntent(Context context, int requestCode, + Intent[] intents, int flags) { + return PendingIntent.getActivities(context, requestCode, intents, flags); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java new file mode 100644 index 00000000000..8b79b1be3df --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class TaskStackBuilderJellybean { + + public static PendingIntent getActivitiesPendingIntent(Context context, int requestCode, + Intent[] intents, int flags, Bundle options) { + return PendingIntent.getActivities(context, requestCode, intents, flags, options); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/package.html b/mobile/google-android-support-v4/src/android/support/v4/app/package.html new file mode 100644 index 00000000000..02d1b79ce8c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/package.html @@ -0,0 +1,8 @@ + + +Support android.app classes to assist with development of applications for +android API level 4 or later. The main features here are backwards-compatible +versions of {@link android.support.v4.app.FragmentManager} and +{@link android.support.v4.app.LoaderManager}. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java b/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java new file mode 100644 index 00000000000..22fe3a223a6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content; + +import android.content.Context; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.util.TimeUtils; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.concurrent.CountDownLatch; + +/** + * Static library support version of the framework's {@link android.content.AsyncTaskLoader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class AsyncTaskLoader extends Loader { + static final String TAG = "AsyncTaskLoader"; + static final boolean DEBUG = false; + + final class LoadTask extends ModernAsyncTask implements Runnable { + + D result; + boolean waiting; + + private CountDownLatch done = new CountDownLatch(1); + + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + if (DEBUG) Log.v(TAG, this + " >>> doInBackground"); + result = AsyncTaskLoader.this.onLoadInBackground(); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground"); + return result; + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + if (DEBUG) Log.v(TAG, this + " onPostExecute"); + try { + AsyncTaskLoader.this.dispatchOnLoadComplete(this, data); + } finally { + done.countDown(); + } + } + + @Override + protected void onCancelled() { + if (DEBUG) Log.v(TAG, this + " onCancelled"); + try { + AsyncTaskLoader.this.dispatchOnCancelled(this, result); + } finally { + done.countDown(); + } + } + + @Override + public void run() { + waiting = false; + AsyncTaskLoader.this.executePendingTask(); + } + } + + volatile LoadTask mTask; + volatile LoadTask mCancellingTask; + + long mUpdateThrottle; + long mLastLoadCompleteTime = -10000; + Handler mHandler; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Set amount to throttle updates by. This is the minimum time from + * when the last {@link #onLoadInBackground()} call has completed until + * a new load is scheduled. + * + * @param delayMS Amount of delay, in milliseconds. + */ + public void setUpdateThrottle(long delayMS) { + mUpdateThrottle = delayMS; + if (delayMS != 0) { + mHandler = new Handler(); + } + } + + @Override + protected void onForceLoad() { + super.onForceLoad(); + cancelLoad(); + mTask = new LoadTask(); + if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask); + executePendingTask(); + } + + /** + * Attempt to cancel the current load task. See {@link android.os.AsyncTask#cancel(boolean)} + * for more info. Must be called on the main thread of the process. + * + *

Cancelling is not an immediate operation, since the load is performed + * in a background thread. If there is currently a load in progress, this + * method requests that the load be cancelled, and notes this is the case; + * once the background thread has completed its work its remaining state + * will be cleared. If another load request comes in during this time, + * it will be held until the cancelled load is complete. + * + * @return Returns false if the task could not be cancelled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called; returns + * true otherwise. + */ + public boolean cancelLoad() { + if (DEBUG) Log.v(TAG, "cancelLoad: mTask=" + mTask); + if (mTask != null) { + if (mCancellingTask != null) { + // There was a pending task already waiting for a previous + // one being canceled; just drop it. + if (DEBUG) Log.v(TAG, + "cancelLoad: still waiting for cancelled task; dropping next"); + if (mTask.waiting) { + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + } + mTask = null; + return false; + } else if (mTask.waiting) { + // There is a task, but it is waiting for the time it should + // execute. We can just toss it. + if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it"); + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + mTask = null; + return false; + } else { + boolean cancelled = mTask.cancel(false); + if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled); + if (cancelled) { + mCancellingTask = mTask; + } + mTask = null; + return cancelled; + } + } + return false; + } + + /** + * Called if the task was canceled before it was completed. Gives the class a chance + * to properly dispose of the result. + */ + public void onCanceled(D data) { + } + + void executePendingTask() { + if (mCancellingTask == null && mTask != null) { + if (mTask.waiting) { + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + } + if (mUpdateThrottle > 0) { + long now = SystemClock.uptimeMillis(); + if (now < (mLastLoadCompleteTime+mUpdateThrottle)) { + // Not yet time to do another load. + if (DEBUG) Log.v(TAG, "Waiting until " + + (mLastLoadCompleteTime+mUpdateThrottle) + + " to execute: " + mTask); + mTask.waiting = true; + mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle); + return; + } + } + if (DEBUG) Log.v(TAG, "Executing: " + mTask); + mTask.executeOnExecutor(ModernAsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + } + + void dispatchOnCancelled(LoadTask task, D data) { + onCanceled(data); + if (mCancellingTask == task) { + if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!"); + rollbackContentChanged(); + mLastLoadCompleteTime = SystemClock.uptimeMillis(); + mCancellingTask = null; + executePendingTask(); + } + } + + void dispatchOnLoadComplete(LoadTask task, D data) { + if (mTask != task) { + if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel"); + dispatchOnCancelled(task, data); + } else { + if (isAbandoned()) { + // This cursor has been abandoned; just cancel the new data. + onCanceled(data); + } else { + commitContentChanged(); + mLastLoadCompleteTime = SystemClock.uptimeMillis(); + mTask = null; + if (DEBUG) Log.v(TAG, "Delivering result"); + deliverResult(data); + } + } + } + + /** + */ + public abstract D loadInBackground(); + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * result directly, but should return them from this method, which will eventually end up + * calling {@link #deliverResult} on the UI thread. If implementations need to process + * the results on the UI thread they may override {@link #deliverResult} and do so + * there. + * + * @return Implementations must return the result of their load operation. + */ + protected D onLoadInBackground() { + return loadInBackground(); + } + + /** + * Locks the current thread until the loader completes the current load + * operation. Returns immediately if there is no load operation running. + * Should not be called from the UI thread: calling it from the UI + * thread would cause a deadlock. + *

+ * Use for testing only. Never call this from a UI thread. + * + * @hide + */ + public void waitForLoader() { + LoadTask task = mTask; + if (task != null) { + try { + task.done.await(); + } catch (InterruptedException e) { + // Ignore + } + } + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + if (mTask != null) { + writer.print(prefix); writer.print("mTask="); writer.print(mTask); + writer.print(" waiting="); writer.println(mTask.waiting); + } + if (mCancellingTask != null) { + writer.print(prefix); writer.print("mCancellingTask="); writer.print(mCancellingTask); + writer.print(" waiting="); writer.println(mCancellingTask.waiting); + } + if (mUpdateThrottle != 0) { + writer.print(prefix); writer.print("mUpdateThrottle="); + TimeUtils.formatDuration(mUpdateThrottle, writer); + writer.print(" mLastLoadCompleteTime="); + TimeUtils.formatDuration(mLastLoadCompleteTime, + SystemClock.uptimeMillis(), writer); + writer.println(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java new file mode 100644 index 00000000000..5c632dd5a51 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java @@ -0,0 +1,389 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.StatFs; +import android.support.v4.os.EnvironmentCompat; +import android.util.Log; + +import java.io.File; + +/** + * Helper for accessing features in {@link android.content.Context} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ContextCompat { + private static final String TAG = "ContextCompat"; + + private static final String DIR_ANDROID = "Android"; + private static final String DIR_DATA = "data"; + private static final String DIR_OBB = "obb"; + private static final String DIR_FILES = "files"; + private static final String DIR_CACHE = "cache"; + + /** + * Start a set of activities as a synthesized task stack, if able. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

startActivities provides a mechanism for constructing a synthetic task stack of + * multiple activities. If the underlying API is not available on the system this method + * will return false.

+ * + * @param context Start activities using this activity as the starting context + * @param intents Array of intents defining the activities that will be started. The element + * length-1 will correspond to the top activity on the resulting task stack. + * @return true if the underlying API was available and the call was successful, false otherwise + */ + public static boolean startActivities(Context context, Intent[] intents) { + return startActivities(context, intents, null); + } + + /** + * Start a set of activities as a synthesized task stack, if able. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

startActivities provides a mechanism for constructing a synthetic task stack of + * multiple activities. If the underlying API is not available on the system this method + * will return false.

+ * + * @param context Start activities using this activity as the starting context + * @param intents Array of intents defining the activities that will be started. The element + * length-1 will correspond to the top activity on the resulting task stack. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * @return true if the underlying API was available and the call was successful, false otherwise + */ + public static boolean startActivities(Context context, Intent[] intents, + Bundle options) { + final int version = Build.VERSION.SDK_INT; + if (version >= 16) { + ContextCompatJellybean.startActivities(context, intents, options); + return true; + } else if (version >= 11) { + ContextCompatHoneycomb.startActivities(context, intents); + return true; + } + return false; + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application's OBB files (if there are + * any) can be found. Note if the application does not have any OBB files, + * these directories may not exist. + *

+ * This is like {@link Context#getFilesDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as {@link Context#getObbDir()}. + * Returned paths may be {@code null} if a storage device is unavailable. + * + * @see Context#getObbDir() + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getObbDirs(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getObbDirs(context); + } else { + final File single; + if (version >= 11) { + single = ContextCompatHoneycomb.getObbDir(context); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_OBB, + context.getPackageName()); + } + return new File[] { single }; + } + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application can place persistent files + * it owns. These files are internal to the application, and not typically + * visible to the user as media. + *

+ * This is like {@link Context#getFilesDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as + * {@link Context#getExternalFilesDir(String)}. Returned paths may be + * {@code null} if a storage device is unavailable. + * + * @see Context#getExternalFilesDir(String) + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getExternalFilesDirs(Context context, String type) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getExternalFilesDirs(context, type); + } else { + final File single; + if (version >= 8) { + single = ContextCompatFroyo.getExternalFilesDir(context, type); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_DATA, + context.getPackageName(), DIR_FILES, type); + } + return new File[] { single }; + } + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application can place cache files it + * owns. These files are internal to the application, and not typically + * visible to the user as media. + *

+ * This is like {@link Context#getCacheDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as + * {@link Context#getExternalCacheDir()}. Returned paths may be {@code null} + * if a storage device is unavailable. + * + * @see Context#getExternalCacheDir() + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getExternalCacheDirs(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getExternalCacheDirs(context); + } else { + final File single; + if (version >= 8) { + single = ContextCompatFroyo.getExternalCacheDir(context); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_DATA, + context.getPackageName(), DIR_CACHE); + } + return new File[] { single }; + } + } + + private static File buildPath(File base, String... segments) { + File cur = base; + for (String segment : segments) { + if (cur == null) { + cur = new File(segment); + } else if (segment != null) { + cur = new File(cur, segment); + } + } + return cur; + } + + /** + * Return a drawable object associated with a particular resource ID. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the returned + * drawable will be styled for the specified Context's theme. + * + * @param id The desired resource identifier, as generated by the aapt tool. + * This integer encodes the package, type, and resource entry. + * The value 0 is an invalid identifier. + * @return Drawable An object that can be used to draw this resource. + */ + public static final Drawable getDrawable(Context context, int id) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getDrawable(context, id); + } else { + return context.getResources().getDrawable(id); + } + } + + /** + * Returns the absolute path to the directory on the filesystem similar to + * {@link Context#getFilesDir()}. The difference is that files placed under this + * directory will be excluded from automatic backup to remote storage on + * devices running {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later. See + * {@link android.app.backup.BackupAgent BackupAgent} for a full discussion + * of the automatic backup mechanism in Android. + * + *

No permissions are required to read or write to the returned path, since this + * path is internal storage. + * + * @return The path of the directory holding application files that will not be + * automatically backed up to remote storage. + * + * @see android.content.Context.getFilesDir + */ + public final File getNoBackupFilesDir(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getNoBackupFilesDir(context); + } else { + ApplicationInfo appInfo = context.getApplicationInfo(); + return createFilesDir(new File(appInfo.dataDir, "no_backup")); + } + } + + /** + * Returns the absolute path to the application specific cache directory on + * the filesystem designed for storing cached code. On devices running + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, the system will delete + * any files stored in this location both when your specific application is + * upgraded, and when the entire platform is upgraded. + *

+ * This location is optimal for storing compiled or optimized code generated + * by your application at runtime. + *

+ * Apps require no extra permissions to read or write to the returned path, + * since this path lives in their private storage. + * + * @return The path of the directory holding application code cache files. + */ + public final File getCodeCacheDir(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getCodeCacheDir(context); + } else { + ApplicationInfo appInfo = context.getApplicationInfo(); + return createFilesDir(new File(appInfo.dataDir, "code_cache")); + } + } + + private synchronized static File createFilesDir(File file) { + if (!file.exists()) { + if (!file.mkdirs()) { + if (file.exists()) { + // spurious failure; probably racing with another process for this app + return file; + } + Log.w(TAG, "Unable to create files subdir " + file.getPath()); + return null; + } + } + return file; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java new file mode 100644 index 00000000000..cf93924db41 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import java.io.File; + +class ContextCompatApi21 { + public static Drawable getDrawable(Context context, int id) { + return context.getDrawable(id); + } + + public static File getNoBackupFilesDir(Context context) { + return context.getNoBackupFilesDir(); + } + + public static File getCodeCacheDir(Context context) { + return context.getCodeCacheDir(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java new file mode 100644 index 00000000000..1fc2f8d8f5d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; + +import java.io.File; + +class ContextCompatFroyo { + public static File getExternalCacheDir(Context context) { + return context.getExternalCacheDir(); + } + + public static File getExternalFilesDir(Context context, String type) { + return context.getExternalFilesDir(type); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java new file mode 100644 index 00000000000..9abc97d3208 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; + +import java.io.File; + +/** + * Implementation of context compatibility that can call Honeycomb APIs. + */ +class ContextCompatHoneycomb { + + static void startActivities(Context context, Intent[] intents) { + context.startActivities(intents); + } + + public static File getObbDir(Context context) { + return context.getObbDir(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java new file mode 100644 index 00000000000..c353b96308f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class ContextCompatJellybean { + + public static void startActivities(Context context, Intent[] intents, Bundle options) { + context.startActivities(intents, options); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java new file mode 100644 index 00000000000..6b58827bf4f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Context; + +import java.io.File; + +class ContextCompatKitKat { + public static File[] getExternalCacheDirs(Context context) { + return context.getExternalCacheDirs(); + } + + public static File[] getExternalFilesDirs(Context context, String type) { + return context.getExternalFilesDirs(type); + } + + public static File[] getObbDirs(Context context) { + return context.getObbDirs(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java b/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java new file mode 100644 index 00000000000..980e7d96f02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; + +/** + * Static library support version of the framework's {@link android.content.CursorLoader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class CursorLoader extends AsyncTaskLoader { + final ForceLoadContentObserver mObserver; + + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + Cursor mCursor; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Creates an empty unspecified CursorLoader. You must follow this with + * calls to {@link #setUri(Uri)}, {@link #setSelection(String)}, etc + * to specify the query to perform. + */ + public CursorLoader(Context context) { + super(context); + mObserver = new ForceLoadContentObserver(); + } + + /** + * Creates a fully-specified CursorLoader. See + * {@link android.content.ContentResolver#query(Uri, String[], String, String[], String) + * ContentResolver.query()} for documentation on the meaning of the + * parameters. These will be passed as-is to that call. + */ + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.print(prefix); writer.print("mUri="); writer.println(mUri); + writer.print(prefix); writer.print("mProjection="); + writer.println(Arrays.toString(mProjection)); + writer.print(prefix); writer.print("mSelection="); writer.println(mSelection); + writer.print(prefix); writer.print("mSelectionArgs="); + writer.println(Arrays.toString(mSelectionArgs)); + writer.print(prefix); writer.print("mSortOrder="); writer.println(mSortOrder); + writer.print(prefix); writer.print("mCursor="); writer.println(mCursor); + writer.print(prefix); writer.print("mContentChanged="); writer.println(mContentChanged); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java b/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java new file mode 100644 index 00000000000..515ce1cd38a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java @@ -0,0 +1,773 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing + * of files associated with an app by creating a content:// {@link Uri} for a file + * instead of a file:/// {@link Uri}. + *

+ * A content URI allows you to grant read and write access using + * temporary access permissions. When you create an {@link Intent} containing + * a content URI, in order to send the content URI + * to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add + * permissions. These permissions are available to the client app for as long as the stack for + * a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a + * {@link android.app.Service}, the permissions are available as long as the + * {@link android.app.Service} is running. + *

+ * In comparison, to control access to a file:/// {@link Uri} you have to modify the + * file system permissions of the underlying file. The permissions you provide become available to + * any app, and remain in effect until you change them. This level of access is + * fundamentally insecure. + *

+ * The increased level of file access security offered by a content URI + * makes FileProvider a key part of Android's security infrastructure. + *

+ * This overview of FileProvider includes the following topics: + *

+ *
    + *
  1. Defining a FileProvider
  2. + *
  3. Specifying Available Files
  4. + *
  5. Retrieving the Content URI for a File
  6. + *
  7. Granting Temporary Permissions to a URI
  8. + *
  9. Serving a Content URI to Another App
  10. + *
+ *

Defining a FileProvider

+ *

+ * Since the default functionality of FileProvider includes content URI generation for files, you + * don't need to define a subclass in code. Instead, you can include a FileProvider in your app + * by specifying it entirely in XML. To specify the FileProvider component itself, add a + * <provider> + * element to your app manifest. Set the android:name attribute to + * android.support.v4.content.FileProvider. Set the android:authorities + * attribute to a URI authority based on a domain you control; for example, if you control the + * domain mydomain.com you should use the authority + * com.mydomain.fileprovider. Set the android:exported attribute to + * false; the FileProvider does not need to be public. Set the + * android:grantUriPermissions attribute to true, to allow you + * to grant temporary access to files. For example: + *

+ *<manifest>
+ *    ...
+ *    <application>
+ *        ...
+ *        <provider
+ *            android:name="android.support.v4.content.FileProvider"
+ *            android:authorities="com.mydomain.fileprovider"
+ *            android:exported="false"
+ *            android:grantUriPermissions="true">
+ *            ...
+ *        </provider>
+ *        ...
+ *    </application>
+ *</manifest>
+ *

+ * If you want to override any of the default behavior of FileProvider methods, extend + * the FileProvider class and use the fully-qualified class name in the android:name + * attribute of the <provider> element. + *

Specifying Available Files

+ * A FileProvider can only generate a content URI for files in directories that you specify + * beforehand. To specify a directory, specify the its storage area and path in XML, using child + * elements of the <paths> element. + * For example, the following paths element tells FileProvider that you intend to + * request content URIs for the images/ subdirectory of your private file area. + *
+ *<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ *    <files-path name="my_images" path="images/"/>
+ *    ...
+ *</paths>
+ *
+ *

+ * The <paths> element must contain one or more of the following child elements: + *

+ *
+ *
+ *
+ *<files-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the files/ subdirectory of your app's internal storage + * area. This subdirectory is the same as the value returned by {@link Context#getFilesDir() + * Context.getFilesDir()}. + *
+ *
+ *<external-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the root of your app's external storage area. The path + * {@link Context#getExternalFilesDir(String) Context.getExternalFilesDir()} returns the + * files/ subdirectory of this this root. + *
+ *
+ *
+ *<cache-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the cache subdirectory of your app's internal storage area. The root path + * of this subdirectory is the same as the value returned by {@link Context#getCacheDir() + * getCacheDir()}. + *
+ *
+ *

+ * These child elements all use the same attributes: + *

+ *
+ *
+ * name="name" + *
+ *
+ * A URI path segment. To enforce security, this value hides the name of the subdirectory + * you're sharing. The subdirectory name for this value is contained in the + * path attribute. + *
+ *
+ * path="path" + *
+ *
+ * The subdirectory you're sharing. While the name attribute is a URI path + * segment, the path value is an actual subdirectory name. Notice that the + * value refers to a subdirectory, not an individual file or files. You can't + * share a single file by its file name, nor can you specify a subset of files using + * wildcards. + *
+ *
+ *

+ * You must specify a child element of <paths> for each directory that contains + * files for which you want content URIs. For example, these XML elements specify two directories: + *

+ *<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ *    <files-path name="my_images" path="images/"/>
+ *    <files-path name="my_docs" path="docs/"/>
+ *</paths>
+ *
+ *

+ * Put the <paths> element and its children in an XML file in your project. + * For example, you can add them to a new file called res/xml/file_paths.xml. + * To link this file to the FileProvider, add a + * <meta-data> element + * as a child of the <provider> element that defines the FileProvider. Set the + * <meta-data> element's "android:name" attribute to + * android.support.FILE_PROVIDER_PATHS. Set the element's "android:resource" attribute + * to @xml/file_paths (notice that you don't specify the .xml + * extension). For example: + *

+ *<provider
+ *    android:name="android.support.v4.content.FileProvider"
+ *    android:authorities="com.mydomain.fileprovider"
+ *    android:exported="false"
+ *    android:grantUriPermissions="true">
+ *    <meta-data
+ *        android:name="android.support.FILE_PROVIDER_PATHS"
+ *        android:resource="@xml/file_paths" />
+ *</provider>
+ *
+ *

Generating the Content URI for a File

+ *

+ * To share a file with another app using a content URI, your app has to generate the content URI. + * To generate the content URI, create a new {@link File} for the file, then pass the {@link File} + * to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI + * returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an + * {@link android.content.Intent}. The client app that receives the content URI can open the file + * and access its contents by calling + * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) + * ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}. + *

+ * For example, suppose your app is offering files to other apps with a FileProvider that has the + * authority com.mydomain.fileprovider. To get a content URI for the file + * default_image.jpg in the images/ subdirectory of your internal storage + * add the following code: + *

+ *File imagePath = new File(Context.getFilesDir(), "images");
+ *File newFile = new File(imagePath, "default_image.jpg");
+ *Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
+ *
+ * As a result of the previous snippet, + * {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI + * content://com.mydomain.fileprovider/my_images/default_image.jpg. + *

Granting Temporary Permissions to a URI

+ * To grant an access permission to a content URI returned from + * {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following: + *
    + *
  • + * Call the method + * {@link Context#grantUriPermission(String, Uri, int) + * Context.grantUriPermission(package, Uri, mode_flags)} for the content:// + * {@link Uri}, using the desired mode flags. This grants temporary access permission for the + * content URI to the specified package, according to the value of the + * the mode_flags parameter, which you can set to + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} + * or both. The permission remains in effect until you revoke it by calling + * {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device + * reboots. + *
  • + *
  • + * Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}. + *
  • + *
  • + * Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both. + *
  • + *
  • + * Finally, send the {@link Intent} to + * another app. Most often, you do this by calling + * {@link android.app.Activity#setResult(int, android.content.Intent) setResult()}. + *

    + * Permissions granted in an {@link Intent} remain in effect while the stack of the receiving + * {@link android.app.Activity} is active. When the stack finishes, the permissions are + * automatically removed. Permissions granted to one {@link android.app.Activity} in a client + * app are automatically extended to other components of that app. + *

    + *
  • + *
+ *

Serving a Content URI to Another App

+ *

+ * There are a variety of ways to serve the content URI for a file to a client app. One common way + * is for the client app to start your app by calling + * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()}, + * which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app. + * In response, your app can immediately return a content URI to the client app or present a user + * interface that allows the user to pick a file. In the latter case, once the user picks the file + * your app can return its content URI. In both cases, your app returns the content URI in an + * {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}. + *

+ *

+ * You can also put the content URI in a {@link android.content.ClipData} object and then add the + * object to an {@link Intent} you send to a client app. To do this, call + * {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can + * add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own + * content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent} + * to set temporary access permissions, the same permissions are applied to all of the content + * URIs. + *

+ *

+ * Note: The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is + * only available in platform version 16 (Android 4.1) and later. If you want to maintain + * compatibility with previous versions, you should send one content URI at a time in the + * {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling + * {@link Intent#setData setData()}. + *

+ *

More Information

+ *

+ * To learn more about FileProvider, see the Android training class + * Sharing Files Securely with URIs. + *

+ */ +public class FileProvider extends ContentProvider { + private static final String[] COLUMNS = { + OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; + + private static final String + META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; + + private static final String TAG_ROOT_PATH = "root-path"; + private static final String TAG_FILES_PATH = "files-path"; + private static final String TAG_CACHE_PATH = "cache-path"; + private static final String TAG_EXTERNAL = "external-path"; + + private static final String ATTR_NAME = "name"; + private static final String ATTR_PATH = "path"; + + private static final File DEVICE_ROOT = new File("/"); + + // @GuardedBy("sCache") + private static HashMap sCache = new HashMap(); + + private PathStrategy mStrategy; + + /** + * The default FileProvider implementation does not need to be initialized. If you want to + * override this method, you must provide your own subclass of FileProvider. + */ + @Override + public boolean onCreate() { + return true; + } + + /** + * After the FileProvider is instantiated, this method is called to provide the system with + * information about the provider. + * + * @param context A {@link Context} for the current component. + * @param info A {@link ProviderInfo} for the new provider. + */ + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + + // Sanity check our security + if (info.exported) { + throw new SecurityException("Provider must not be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grant uri permissions"); + } + + mStrategy = getPathStrategy(context, info.authority); + } + + /** + * Return a content URI for a given {@link File}. Specific temporary + * permissions for the content URI can be set with + * {@link Context#grantUriPermission(String, Uri, int)}, or added + * to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then + * {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a + * content {@link Uri} for file paths defined in their <paths> + * meta-data element. See the Class Overview for more information. + * + * @param context A {@link Context} for the current component. + * @param authority The authority of a {@link FileProvider} defined in a + * {@code <provider>} element in your app's manifest. + * @param file A {@link File} pointing to the filename for which you want a + * content {@link Uri}. + * @return A content URI for the file. + * @throws IllegalArgumentException When the given {@link File} is outside + * the paths supported by the provider. + */ + public static Uri getUriForFile(Context context, String authority, File file) { + final PathStrategy strategy = getPathStrategy(context, authority); + return strategy.getUriForFile(file); + } + + /** + * Use a content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file + * managed by the FileProvider. + * FileProvider reports the column names defined in {@link android.provider.OpenableColumns}: + *
    + *
  • {@link android.provider.OpenableColumns#DISPLAY_NAME}
  • + *
  • {@link android.provider.OpenableColumns#SIZE}
  • + *
+ * For more information, see + * {@link ContentProvider#query(Uri, String[], String, String[], String) + * ContentProvider.query()}. + * + * @param uri A content URI returned by {@link #getUriForFile}. + * @param projection The list of columns to put into the {@link Cursor}. If null all columns are + * included. + * @param selection Selection criteria to apply. If null then all data that matches the content + * URI is returned. + * @param selectionArgs An array of {@link java.lang.String}, containing arguments to bind to + * the selection parameter. The query method scans selection from left to + * right and iterates through selectionArgs, replacing the current "?" character in + * selection with the value at the current position in selectionArgs. The + * values are bound to selection as {@link java.lang.String} values. + * @param sortOrder A {@link java.lang.String} containing the column name(s) on which to sort + * the resulting {@link Cursor}. + * @return A {@link Cursor} containing the results of the query. + * + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + + if (projection == null) { + projection = COLUMNS; + } + + String[] cols = new String[projection.length]; + Object[] values = new Object[projection.length]; + int i = 0; + for (String col : projection) { + if (OpenableColumns.DISPLAY_NAME.equals(col)) { + cols[i] = OpenableColumns.DISPLAY_NAME; + values[i++] = file.getName(); + } else if (OpenableColumns.SIZE.equals(col)) { + cols[i] = OpenableColumns.SIZE; + values[i++] = file.length(); + } + } + + cols = copyOf(cols, i); + values = copyOf(values, i); + + final MatrixCursor cursor = new MatrixCursor(cols, 1); + cursor.addRow(values); + return cursor; + } + + /** + * Returns the MIME type of a content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * + * @param uri A content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @return If the associated file has an extension, the MIME type associated with that + * extension; otherwise application/octet-stream. + */ + @Override + public String getType(Uri uri) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + + final int lastDot = file.getName().lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = file.getName().substring(lastDot + 1); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + /** + * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must + * subclass FileProvider if you want to provide different functionality. + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("No external inserts"); + } + + /** + * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must + * subclass FileProvider if you want to provide different functionality. + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("No external updates"); + } + + /** + * Deletes the file associated with the specified content URI, as + * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this + * method does not throw an {@link java.io.IOException}; you must check its return value. + * + * @param uri A content URI for a file, as returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @param selection Ignored. Set to {@code null}. + * @param selectionArgs Ignored. Set to {@code null}. + * @return 1 if the delete succeeds; otherwise, 0. + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + return file.delete() ? 1 : 0; + } + + /** + * By default, FileProvider automatically returns the + * {@link ParcelFileDescriptor} for a file associated with a content:// + * {@link Uri}. To get the {@link ParcelFileDescriptor}, call + * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) + * ContentResolver.openFileDescriptor}. + * + * To override this method, you must provide your own subclass of FileProvider. + * + * @param uri A content URI associated with a file, as returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and + * write access, or "rwt" for read and write access that truncates any existing file. + * @return A new {@link ParcelFileDescriptor} with which you can access the file. + */ + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + final int fileMode = modeToMode(mode); + return ParcelFileDescriptor.open(file, fileMode); + } + + /** + * Return {@link PathStrategy} for given authority, either by parsing or + * returning from cache. + */ + private static PathStrategy getPathStrategy(Context context, String authority) { + PathStrategy strat; + synchronized (sCache) { + strat = sCache.get(authority); + if (strat == null) { + try { + strat = parsePathStrategy(context, authority); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); + } catch (XmlPullParserException e) { + throw new IllegalArgumentException( + "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); + } + sCache.put(authority, strat); + } + } + return strat; + } + + /** + * Parse and return {@link PathStrategy} for given authority as defined in + * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}. + * + * @see #getPathStrategy(Context, String) + */ + private static PathStrategy parsePathStrategy(Context context, String authority) + throws IOException, XmlPullParserException { + final SimplePathStrategy strat = new SimplePathStrategy(authority); + + final ProviderInfo info = context.getPackageManager() + .resolveContentProvider(authority, PackageManager.GET_META_DATA); + final XmlResourceParser in = info.loadXmlMetaData( + context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); + if (in == null) { + throw new IllegalArgumentException( + "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); + } + + int type; + while ((type = in.next()) != END_DOCUMENT) { + if (type == START_TAG) { + final String tag = in.getName(); + + final String name = in.getAttributeValue(null, ATTR_NAME); + String path = in.getAttributeValue(null, ATTR_PATH); + + File target = null; + if (TAG_ROOT_PATH.equals(tag)) { + target = buildPath(DEVICE_ROOT, path); + } else if (TAG_FILES_PATH.equals(tag)) { + target = buildPath(context.getFilesDir(), path); + } else if (TAG_CACHE_PATH.equals(tag)) { + target = buildPath(context.getCacheDir(), path); + } else if (TAG_EXTERNAL.equals(tag)) { + target = buildPath(Environment.getExternalStorageDirectory(), path); + } + + if (target != null) { + strat.addRoot(name, target); + } + } + } + + return strat; + } + + /** + * Strategy for mapping between {@link File} and {@link Uri}. + *

+ * Strategies must be symmetric so that mapping a {@link File} to a + * {@link Uri} and then back to a {@link File} points at the original + * target. + *

+ * Strategies must remain consistent across app launches, and not rely on + * dynamic state. This ensures that any generated {@link Uri} can still be + * resolved if your process is killed and later restarted. + * + * @see SimplePathStrategy + */ + interface PathStrategy { + /** + * Return a {@link Uri} that represents the given {@link File}. + */ + public Uri getUriForFile(File file); + + /** + * Return a {@link File} that represents the given {@link Uri}. + */ + public File getFileForUri(Uri uri); + } + + /** + * Strategy that provides access to files living under a narrow whitelist of + * filesystem roots. It will throw {@link SecurityException} if callers try + * accessing files outside the configured roots. + *

+ * For example, if configured with + * {@code addRoot("myfiles", context.getFilesDir())}, then + * {@code context.getFileStreamPath("foo.txt")} would map to + * {@code content://myauthority/myfiles/foo.txt}. + */ + static class SimplePathStrategy implements PathStrategy { + private final String mAuthority; + private final HashMap mRoots = new HashMap(); + + public SimplePathStrategy(String authority) { + mAuthority = authority; + } + + /** + * Add a mapping from a name to a filesystem root. The provider only offers + * access to files that live under configured roots. + */ + public void addRoot(String name, File root) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("Name must not be empty"); + } + + try { + // Resolve to canonical path to keep path checking fast + root = root.getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to resolve canonical path for " + root, e); + } + + mRoots.put(name, root); + } + + @Override + public Uri getUriForFile(File file) { + String path; + try { + path = file.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve canonical path for " + file); + } + + // Find the most-specific root path + Map.Entry mostSpecific = null; + for (Map.Entry root : mRoots.entrySet()) { + final String rootPath = root.getValue().getPath(); + if (path.startsWith(rootPath) && (mostSpecific == null + || rootPath.length() > mostSpecific.getValue().getPath().length())) { + mostSpecific = root; + } + } + + if (mostSpecific == null) { + throw new IllegalArgumentException( + "Failed to find configured root that contains " + path); + } + + // Start at first char of path under root + final String rootPath = mostSpecific.getValue().getPath(); + if (rootPath.endsWith("/")) { + path = path.substring(rootPath.length()); + } else { + path = path.substring(rootPath.length() + 1); + } + + // Encode the tag and path separately + path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); + return new Uri.Builder().scheme("content") + .authority(mAuthority).encodedPath(path).build(); + } + + @Override + public File getFileForUri(Uri uri) { + String path = uri.getEncodedPath(); + + final int splitIndex = path.indexOf('/', 1); + final String tag = Uri.decode(path.substring(1, splitIndex)); + path = Uri.decode(path.substring(splitIndex + 1)); + + final File root = mRoots.get(tag); + if (root == null) { + throw new IllegalArgumentException("Unable to find configured root for " + uri); + } + + File file = new File(root, path); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve canonical path for " + file); + } + + if (!file.getPath().startsWith(root.getPath())) { + throw new SecurityException("Resolved path jumped beyond configured root"); + } + + return file; + } + } + + /** + * Copied from ContentResolver.java + */ + private static int modeToMode(String mode) { + int modeBits; + if ("r".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_ONLY; + } else if ("w".equals(mode) || "wt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else if ("wa".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_APPEND; + } else if ("rw".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE; + } else if ("rwt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else { + throw new IllegalArgumentException("Invalid mode: " + mode); + } + return modeBits; + } + + private static File buildPath(File base, String... segments) { + File cur = base; + for (String segment : segments) { + if (segment != null) { + cur = new File(cur, segment); + } + } + return cur; + } + + private static String[] copyOf(String[] original, int newLength) { + final String[] result = new String[newLength]; + System.arraycopy(original, 0, result, 0, newLength); + return result; + } + + private static Object[] copyOf(Object[] original, int newLength) { + final Object[] result = new Object[newLength]; + System.arraycopy(original, 0, result, 0, newLength); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java new file mode 100644 index 00000000000..11d333443cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.content.Intent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class IntentCompat { + + interface IntentCompatImpl { + Intent makeMainActivity(ComponentName componentName); + Intent makeMainSelectorActivity(String selectorAction, String selectorCategory); + Intent makeRestartActivityTask(ComponentName mainActivity); + } + + static class IntentCompatImplBase implements IntentCompatImpl { + @Override + public Intent makeMainActivity(ComponentName componentName) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(componentName); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + return intent; + } + + @Override + public Intent makeMainSelectorActivity(String selectorAction, + String selectorCategory) { + // Before api 15 you couldn't set a selector intent. + // Fall back and just return an intent with the requested action/category, + // even though it won't be a proper "main" intent. + Intent intent = new Intent(selectorAction); + intent.addCategory(selectorCategory); + return intent; + } + + @Override + public Intent makeRestartActivityTask(ComponentName mainActivity) { + Intent intent = makeMainActivity(mainActivity); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } + } + + static class IntentCompatImplHC extends IntentCompatImplBase { + @Override + public Intent makeMainActivity(ComponentName componentName) { + return IntentCompatHoneycomb.makeMainActivity(componentName); + } + @Override + public Intent makeRestartActivityTask(ComponentName componentName) { + return IntentCompatHoneycomb.makeRestartActivityTask(componentName); + } + } + + static class IntentCompatImplIcsMr1 extends IntentCompatImplHC { + @Override + public Intent makeMainSelectorActivity(String selectorAction, String selectorCategory) { + return IntentCompatIcsMr1.makeMainSelectorActivity(selectorAction, selectorCategory); + } + } + + private static final IntentCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 15) { + IMPL = new IntentCompatImplIcsMr1(); + } else if (version >= 11) { + IMPL = new IntentCompatImplHC(); + } else { + IMPL = new IntentCompatImplBase(); + } + } + + private IntentCompat() { + /* Hide constructor */ + } + + /** + * Broadcast Action: Resources for a set of packages (which were + * previously unavailable) are currently + * available since the media on which they exist is available. + * The extra data {@link #EXTRA_CHANGED_PACKAGE_LIST} contains a + * list of packages whose availability changed. + * The extra data {@link #EXTRA_CHANGED_UID_LIST} contains a + * list of uids of packages whose availability changed. + * Note that the + * packages in this list do not receive this broadcast. + * The specified set of packages are now available on the system. + *

Includes the following extras: + *

    + *
  • {@link #EXTRA_CHANGED_PACKAGE_LIST} is the set of packages + * whose resources(were previously unavailable) are currently available. + * {@link #EXTRA_CHANGED_UID_LIST} is the set of uids of the + * packages whose resources(were previously unavailable) + * are currently available. + *
+ * + *

This is a protected intent that can only be sent + * by the system. + */ + public static final String ACTION_EXTERNAL_APPLICATIONS_AVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE"; + + /** + * Broadcast Action: Resources for a set of packages are currently + * unavailable since the media on which they exist is unavailable. + * The extra data {@link #EXTRA_CHANGED_PACKAGE_LIST} contains a + * list of packages whose availability changed. + * The extra data {@link #EXTRA_CHANGED_UID_LIST} contains a + * list of uids of packages whose availability changed. + * The specified set of packages can no longer be + * launched and are practically unavailable on the system. + *

Inclues the following extras: + *

    + *
  • {@link #EXTRA_CHANGED_PACKAGE_LIST} is the set of packages + * whose resources are no longer available. + * {@link #EXTRA_CHANGED_UID_LIST} is the set of packages + * whose resources are no longer available. + *
+ * + *

This is a protected intent that can only be sent + * by the system. + */ + public static final String ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE"; + + /** + * This field is part of + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} + * and contains a string array of all of the components that have changed. + */ + public static final String EXTRA_CHANGED_PACKAGE_LIST = + "android.intent.extra.changed_package_list"; + + /** + * This field is part of + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} + * and contains an integer array of uids of all of the components + * that have changed. + */ + public static final String EXTRA_CHANGED_UID_LIST = + "android.intent.extra.changed_uid_list"; + + /** + * A constant String that is associated with the Intent, used with + * {@link android.content.Intent#ACTION_SEND} to supply an alternative to + * {@link android.content.Intent#EXTRA_TEXT} + * as HTML formatted text. Note that you must also supply + * {@link android.content.Intent#EXTRA_TEXT}. + */ + public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; + + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause a newly launching task to be placed on top of the current + * home activity task (if there is one). That is, pressing back from the task + * will always return the user to home even if that was not the last activity they + * saw. This can only be used in conjunction with + * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK}. + */ + public static final int FLAG_ACTIVITY_TASK_ON_HOME = 0x00004000; + + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause any existing task that would be associated with the + * activity to be cleared before the activity is started. That is, the activity + * becomes the new root of an otherwise empty task, and any old activities + * are finished. This can only be used in conjunction with + * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK}. + * + *

This flag will only be obeyed on devices supporting API 11 or higher.

+ */ + public static final int FLAG_ACTIVITY_CLEAR_TASK = 0x00008000; + + /** + * Create an intent to launch the main (root) activity of a task. This + * is the Intent that is started when the application's is launched from + * Home. For anything else that wants to launch an application in the + * same way, it is important that they use an Intent structured the same + * way, and can use this function to ensure this is the case. + * + *

The returned Intent has the given Activity component as its explicit + * component, {@link Intent#ACTION_MAIN ACTION_MAIN} as its action, and includes the + * category {@link Intent#CATEGORY_LAUNCHER CATEGORY_LAUNCHER}. This does not have + * {@link Intent#FLAG_ACTIVITY_NEW_TASK FLAG_ACTIVITY_NEW_TASK} set, + * though typically you will want to do that through {@link Intent#addFlags(int) addFlags(int)} + * on the returned Intent. + * + * @param mainActivity The main activity component that this Intent will + * launch. + * @return Returns a newly created Intent that can be used to launch the + * activity as a main application entry. + * + * @see Intent#setClass + * @see Intent#setComponent + */ + public static Intent makeMainActivity(ComponentName mainActivity) { + return IMPL.makeMainActivity(mainActivity); + } + + + /** + * Make an Intent for the main activity of an application, without + * specifying a specific activity to run but giving a selector to find + * the activity. This results in a final Intent that is structured + * the same as when the application is launched from + * Home. For anything else that wants to launch an application in the + * same way, it is important that they use an Intent structured the same + * way, and can use this function to ensure this is the case. + * + *

The returned Intent has {@link Intent#ACTION_MAIN} as its action, and includes the + * category {@link Intent#CATEGORY_LAUNCHER}. This does not have + * {@link Intent#FLAG_ACTIVITY_NEW_TASK} set, though typically you will want + * to do that through {@link Intent#addFlags(int)} on the returned Intent. + * + * @param selectorAction The action name of the Intent's selector. + * @param selectorCategory The name of a category to add to the Intent's + * selector. + * @return Returns a newly created Intent that can be used to launch the + * activity as a main application entry. + * + * @see #setSelector(Intent) + */ + public static Intent makeMainSelectorActivity(String selectorAction, + String selectorCategory) { + return IMPL.makeMainSelectorActivity(selectorAction, selectorCategory); + } + + /** + * Make an Intent that can be used to re-launch an application's task + * in its base state. This is like {@link #makeMainActivity(ComponentName)}, + * but also sets the flags {@link Intent#FLAG_ACTIVITY_NEW_TASK} and + * {@link IntentCompat#FLAG_ACTIVITY_CLEAR_TASK}. + * + * @param mainActivity The activity component that is the root of the + * task; this is the activity that has been published in the application's + * manifest as the main launcher icon. + * + * @return Returns a newly created Intent that can be used to relaunch the + * activity's task in its root state. + */ + public static Intent makeRestartActivityTask(ComponentName mainActivity) { + return IMPL.makeRestartActivityTask(mainActivity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java new file mode 100644 index 00000000000..6356898184b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.ComponentName; +import android.content.Intent; + +class IntentCompatHoneycomb { + public static Intent makeMainActivity(ComponentName mainActivity) { + return Intent.makeMainActivity(mainActivity); + } + + public static Intent makeRestartActivityTask(ComponentName mainActivity) { + return Intent.makeRestartActivityTask(mainActivity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java new file mode 100644 index 00000000000..0fa2c433530 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.Intent; + +class IntentCompatIcsMr1 { + + public static Intent makeMainSelectorActivity(String selectorAction, String selectorCategory) { + return Intent.makeMainSelectorActivity(selectorAction, selectorCategory); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java b/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java new file mode 100644 index 00000000000..bd01b0a2033 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.support.v4.util.DebugUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Static library support version of the framework's {@link android.content.Loader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class Loader { + int mId; + OnLoadCompleteListener mListener; + Context mContext; + boolean mStarted = false; + boolean mAbandoned = false; + boolean mReset = true; + boolean mContentChanged = false; + boolean mProcessingChange = false; + + /** + * An implementation of a ContentObserver that takes care of connecting + * it to the Loader to have the loader re-load its data when the observer + * is told it has changed. You do not normally need to use this yourself; + * it is used for you by {@link android.support.v4.content.CursorLoader} + * to take care of executing an update when the cursor's backing data changes. + */ + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + /** + * Interface that is implemented to discover when a Loader has finished + * loading its data. You do not normally need to implement this yourself; + * it is used in the implementation of {@link android.support.v4.app.LoaderManager} + * to find out when a Loader it is managing has completed so that this can + * be reported to its client. This interface should only be used if a + * Loader is not being used in conjunction with LoaderManager. + */ + public interface OnLoadCompleteListener { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the process's main thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. + * The callback will be called on the process's main thread so it's safe to + * pass the results to widgets. + * + *

Must be called from the process's main thread. + */ + public void registerListener(int id, OnLoadCompleteListener listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Remove a listener that was previously added with {@link #registerListener}. + * + * Must be called from the process's main thread. + */ + public void unregisterListener(OnLoadCompleteListener listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Return whether this load has been started. That is, its {@link #startLoading()} + * has been called and no calls to {@link #stopLoading()} or + * {@link #reset()} have yet been made. + */ + public boolean isStarted() { + return mStarted; + } + + /** + * Return whether this loader has been abandoned. In this state, the + * loader must not report any new data, and must keep + * its last reported data valid until it is finally reset. + */ + public boolean isAbandoned() { + return mAbandoned; + } + + /** + * Return whether this load has been reset. That is, either the loader + * has not yet been started for the first time, or its {@link #reset()} + * has been called. + */ + public boolean isReset() { + return mReset; + } + + /** + * Starts an asynchronous load of the Loader's data. When the result + * is ready the callbacks will be called on the process's main thread. + * If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. + * Calling {@link #stopLoading} will stop the delivery of callbacks. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} and {@link #isReset()} will return the correct + * values, and then calls the implementation's {@link #onStartLoading()}. + * + *

Must be called from the process's main thread. + */ + public final void startLoading() { + mStarted = true; + mReset = false; + mAbandoned = false; + onStartLoading(); + } + + /** + * Subclasses must implement this to take care of loading their data, + * as per {@link #startLoading()}. This is not called by clients directly, + * but as a result of a call to {@link #startLoading()}. + */ + protected void onStartLoading() { + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. This simply calls through to the + * implementation's {@link #onForceLoad()}. You generally should only call this + * when the loader is started -- that is, {@link #isStarted()} returns true. + * + *

Must be called from the process's main thread. + */ + public void forceLoad() { + onForceLoad(); + } + + /** + * Subclasses must implement this to take care of requests to {@link #forceLoad()}. + * This will always be called from the process's main thread. + */ + protected void onForceLoad() { + } + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called. + * Implementations should not invalidate their data at this point -- + * clients are still free to use the last data the loader reported. They will, + * however, typically stop reporting new data if the data changes; they can + * still monitor for changes, but must not report them to the client until and + * if {@link #startLoading()} is later called. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} will return the correct + * value, and then calls the implementation's {@link #onStopLoading()}. + * + *

Must be called from the process's main thread. + */ + public void stopLoading() { + mStarted = false; + onStopLoading(); + } + + /** + * Subclasses must implement this to take care of stopping their loader, + * as per {@link #stopLoading()}. This is not called by clients directly, + * but as a result of a call to {@link #stopLoading()}. + * This will always be called from the process's main thread. + */ + protected void onStopLoading() { + } + + /** + * Tell the Loader that it is being abandoned. This is called prior + * to {@link #reset} to have it retain its current data but not report + * any new data. + */ + public void abandon() { + mAbandoned = true; + onAbandon(); + } + + /** + * Subclasses implement this to take care of being abandoned. This is + * an optional intermediate state prior to {@link #onReset()} -- it means that + * the client is no longer interested in any new data from the loader, + * so the loader must not report any further updates. However, the + * loader must keep its last reported data valid until the final + * {@link #onReset()} happens. You can retrieve the current abandoned + * state with {@link #isAbandoned}. + */ + protected void onAbandon() { + } + + /** + * Resets the state of the Loader. The Loader should at this point free + * all of its resources, since it may never be called again; however, its + * {@link #startLoading()} may later be called at which point it must be + * able to start running again. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} and {@link #isReset()} will return the correct + * values, and then calls the implementation's {@link #onReset()}. + * + *

Must be called from the process's main thread. + */ + public void reset() { + onReset(); + mReset = true; + mStarted = false; + mAbandoned = false; + mContentChanged = false; + mProcessingChange = false; + } + + /** + * Subclasses must implement this to take care of resetting their loader, + * as per {@link #reset()}. This is not called by clients directly, + * but as a result of a call to {@link #reset()}. + * This will always be called from the process's main thread. + */ + protected void onReset() { + } + + /** + * Take the current flag indicating whether the loader's content had + * changed while it was stopped. If it had, true is returned and the + * flag is cleared. + */ + public boolean takeContentChanged() { + boolean res = mContentChanged; + mContentChanged = false; + mProcessingChange |= res; + return res; + } + + /** + * Commit that you have actually fully processed a content change that + * was returned by {@link #takeContentChanged}. This is for use with + * {@link #rollbackContentChanged()} to handle situations where a load + * is cancelled. Call this when you have completely processed a load + * without it being cancelled. + */ + public void commitContentChanged() { + mProcessingChange = false; + } + + /** + * Report that you have abandoned the processing of a content change that + * was returned by {@link #takeContentChanged()} and would like to rollback + * to the state where there is again a pending content change. This is + * to handle the case where a data load due to a content change has been + * canceled before its data was delivered back to the loader. + */ + public void rollbackContentChanged() { + if (mProcessingChange) { + mContentChanged = true; + } + } + + /** + * Called when {@link ForceLoadContentObserver} detects a change. The + * default implementation checks to see if the loader is currently started; + * if so, it simply calls {@link #forceLoad()}; otherwise, it sets a flag + * so that {@link #takeContentChanged()} returns true. + * + *

Must be called from the process's main thread. + */ + public void onContentChanged() { + if (mStarted) { + forceLoad(); + } else { + // This loader has been stopped, so we don't want to load + // new data right now... but keep track of it changing to + // refresh later if we start again. + mContentChanged = true; + } + } + + /** + * For debugging, converts an instance of the Loader's data class to + * a string that can be printed. Must handle a null data. + */ + public String dataToString(D data) { + StringBuilder sb = new StringBuilder(64); + DebugUtils.buildShortClassTag(data, sb); + sb.append("}"); + return sb.toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + DebugUtils.buildShortClassTag(this, sb); + sb.append(" id="); + sb.append(mId); + sb.append("}"); + return sb.toString(); + } + + /** + * Print the Loader's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mId="); writer.print(mId); + writer.print(" mListener="); writer.println(mListener); + if (mStarted || mContentChanged || mProcessingChange) { + writer.print(prefix); writer.print("mStarted="); writer.print(mStarted); + writer.print(" mContentChanged="); writer.print(mContentChanged); + writer.print(" mProcessingChange="); writer.println(mProcessingChange); + } + if (mAbandoned || mReset) { + writer.print(prefix); writer.print("mAbandoned="); writer.print(mAbandoned); + writer.print(" mReset="); writer.println(mReset); + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java b/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java new file mode 100644 index 00000000000..7a7c50f4eff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +/** + * Helper to register for and send broadcasts of Intents to local objects + * within your process. This is has a number of advantages over sending + * global broadcasts with {@link android.content.Context#sendBroadcast}: + *

    + *
  • You know that the data you are broadcasting won't leave your app, so + * don't need to worry about leaking private data. + *
  • It is not possible for other applications to send these broadcasts to + * your app, so you don't need to worry about having security holes they can + * exploit. + *
  • It is more efficient than sending a global broadcast through the + * system. + *
+ */ +public class LocalBroadcastManager { + private static class ReceiverRecord { + final IntentFilter filter; + final BroadcastReceiver receiver; + boolean broadcasting; + + ReceiverRecord(IntentFilter _filter, BroadcastReceiver _receiver) { + filter = _filter; + receiver = _receiver; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(128); + builder.append("Receiver{"); + builder.append(receiver); + builder.append(" filter="); + builder.append(filter); + builder.append("}"); + return builder.toString(); + } + } + + private static class BroadcastRecord { + final Intent intent; + final ArrayList receivers; + + BroadcastRecord(Intent _intent, ArrayList _receivers) { + intent = _intent; + receivers = _receivers; + } + } + + private static final String TAG = "LocalBroadcastManager"; + private static final boolean DEBUG = false; + + private final Context mAppContext; + + private final HashMap> mReceivers + = new HashMap>(); + private final HashMap> mActions + = new HashMap>(); + + private final ArrayList mPendingBroadcasts + = new ArrayList(); + + static final int MSG_EXEC_PENDING_BROADCASTS = 1; + + private final Handler mHandler; + + private static final Object mLock = new Object(); + private static LocalBroadcastManager mInstance; + + public static LocalBroadcastManager getInstance(Context context) { + synchronized (mLock) { + if (mInstance == null) { + mInstance = new LocalBroadcastManager(context.getApplicationContext()); + } + return mInstance; + } + } + + private LocalBroadcastManager(Context context) { + mAppContext = context; + mHandler = new Handler(context.getMainLooper()) { + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_EXEC_PENDING_BROADCASTS: + executePendingBroadcasts(); + break; + default: + super.handleMessage(msg); + } + } + }; + } + + /** + * Register a receive for any local broadcasts that match the given IntentFilter. + * + * @param receiver The BroadcastReceiver to handle the broadcast. + * @param filter Selects the Intent broadcasts to be received. + * + * @see #unregisterReceiver + */ + public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + synchronized (mReceivers) { + ReceiverRecord entry = new ReceiverRecord(filter, receiver); + ArrayList filters = mReceivers.get(receiver); + if (filters == null) { + filters = new ArrayList(1); + mReceivers.put(receiver, filters); + } + filters.add(filter); + for (int i=0; i entries = mActions.get(action); + if (entries == null) { + entries = new ArrayList(1); + mActions.put(action, entries); + } + entries.add(entry); + } + } + } + + /** + * Unregister a previously registered BroadcastReceiver. All + * filters that have been registered for this BroadcastReceiver will be + * removed. + * + * @param receiver The BroadcastReceiver to unregister. + * + * @see #registerReceiver + */ + public void unregisterReceiver(BroadcastReceiver receiver) { + synchronized (mReceivers) { + ArrayList filters = mReceivers.remove(receiver); + if (filters == null) { + return; + } + for (int i=0; i receivers = mActions.get(action); + if (receivers != null) { + for (int k=0; k categories = intent.getCategories(); + + final boolean debug = DEBUG || + ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0); + if (debug) Log.v( + TAG, "Resolving type " + type + " scheme " + scheme + + " of intent " + intent); + + ArrayList entries = mActions.get(intent.getAction()); + if (entries != null) { + if (debug) Log.v(TAG, "Action list: " + entries); + + ArrayList receivers = null; + for (int i=0; i= 0) { + if (debug) Log.v(TAG, " Filter matched! match=0x" + + Integer.toHexString(match)); + if (receivers == null) { + receivers = new ArrayList(); + } + receivers.add(receiver); + receiver.broadcasting = true; + } else { + if (debug) { + String reason; + switch (match) { + case IntentFilter.NO_MATCH_ACTION: reason = "action"; break; + case IntentFilter.NO_MATCH_CATEGORY: reason = "category"; break; + case IntentFilter.NO_MATCH_DATA: reason = "data"; break; + case IntentFilter.NO_MATCH_TYPE: reason = "type"; break; + default: reason = "unknown reason"; break; + } + Log.v(TAG, " Filter did not match: " + reason); + } + } + } + + if (receivers != null) { + for (int i=0; iNote that for now this is not publicly available because it is not a + * complete implementation, only sufficient for the needs of + * {@link AsyncTaskLoader}. + */ +abstract class ModernAsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "ModernAsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(10); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static final InternalHandler sHandler = new InternalHandler(); + + private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR; + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link android.os.AsyncTask#onPostExecute(Object)} has finished. + */ + FINISHED, + } + + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public ModernAsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + final Result result = get(); + + postResultIfNotInvoked(result); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } catch (Throwable t) { + throw new RuntimeException("An error occured while executing " + + "doInBackground()", t); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mFuture.isCancelled(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. After + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this + * back to a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on + * its use. + * + *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link android.os.AsyncTask.Status#RUNNING} or + * {@link android.os.AsyncTask.Status#FINISHED}. + */ + public final ModernAsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link Executor} for custom + * behavior. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. + * + *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link android.os.AsyncTask.Status#RUNNING} + * or {@link android.os.AsyncTask.Status#FINISHED}. + */ + public final ModernAsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final ModernAsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(ModernAsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java b/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java new file mode 100644 index 00000000000..9e41f22e826 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java @@ -0,0 +1,134 @@ +/* + * 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. + */ + +package android.support.v4.content; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.util.Log; +import android.util.SparseArray; + +/** + * Helper for the common pattern of implementing a {@link BroadcastReceiver} + * that receives a device wakeup event and then passes the work off + * to a {@link android.app.Service}, while ensuring that the + * device does not go back to sleep during the transition. + * + *

This class takes care of creating and managing a partial wake lock + * for you; you must request the {@link android.Manifest.permission#WAKE_LOCK} + * permission to use it.

+ * + *

Example

+ * + *

A {@link WakefulBroadcastReceiver} uses the method + * {@link WakefulBroadcastReceiver#startWakefulService startWakefulService()} + * to start the service that does the work. This method is comparable to + * {@link android.content.Context#startService startService()}, except that + * the {@link WakefulBroadcastReceiver} is holding a wake lock when the service + * starts. The intent that is passed with + * {@link WakefulBroadcastReceiver#startWakefulService startWakefulService()} + * holds an extra identifying the wake lock.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulReceiver.java complete} + * + *

The service (in this example, an {@link android.app.IntentService}) does + * some work. When it is finished, it releases the wake lock by calling + * {@link WakefulBroadcastReceiver#completeWakefulIntent + * completeWakefulIntent(intent)}. The intent it passes as a parameter + * is the same intent that the {@link WakefulBroadcastReceiver} originally + * passed in.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulService.java complete} + */ +public abstract class WakefulBroadcastReceiver extends BroadcastReceiver { + private static final String EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid"; + + private static final SparseArray mActiveWakeLocks + = new SparseArray(); + private static int mNextId = 1; + + /** + * Do a {@link android.content.Context#startService(android.content.Intent) + * Context.startService}, but holding a wake lock while the service starts. + * This will modify the Intent to hold an extra identifying the wake lock; + * when the service receives it in {@link android.app.Service#onStartCommand + * Service.onStartCommand}, it should pass back the Intent it receives there to + * {@link #completeWakefulIntent(android.content.Intent)} in order to release + * the wake lock. + * + * @param context The Context in which it operate. + * @param intent The Intent with which to start the service, as per + * {@link android.content.Context#startService(android.content.Intent) + * Context.startService}. + */ + public static ComponentName startWakefulService(Context context, Intent intent) { + synchronized (mActiveWakeLocks) { + int id = mNextId; + mNextId++; + if (mNextId <= 0) { + mNextId = 1; + } + + intent.putExtra(EXTRA_WAKE_LOCK_ID, id); + ComponentName comp = context.startService(intent); + if (comp == null) { + return null; + } + + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "wake:" + comp.flattenToShortString()); + wl.setReferenceCounted(false); + wl.acquire(60*1000); + mActiveWakeLocks.put(id, wl); + return comp; + } + } + + /** + * Finish the execution from a previous {@link #startWakefulService}. Any wake lock + * that was being held will now be released. + * + * @param intent The Intent as originally generated by {@link #startWakefulService}. + * @return Returns true if the intent is associated with a wake lock that is + * now released; returns false if there was no wake lock specified for it. + */ + public static boolean completeWakefulIntent(Intent intent) { + final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0); + if (id == 0) { + return false; + } + synchronized (mActiveWakeLocks) { + PowerManager.WakeLock wl = mActiveWakeLocks.get(id); + if (wl != null) { + wl.release(); + mActiveWakeLocks.remove(id); + return true; + } + // We return true whether or not we actually found the wake lock + // the return code is defined to indicate whether the Intent contained + // an identifier for a wake lock that it was supposed to match. + // We just log a warning here if there is no wake lock found, which could + // happen for example if this function is called twice on the same + // intent or the process is killed and restarted before processing the intent. + Log.w("WakefulBroadcastReceiver", "No active wake lock id #" + id); + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/package.html b/mobile/google-android-support-v4/src/android/support/v4/content/package.html new file mode 100644 index 00000000000..33bf4b5ccd9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/package.html @@ -0,0 +1,10 @@ + + +Support android.content classes to assist with development of applications for +android API level 4 or later. The main features here are +{@link android.support.v4.content.Loader} and related classes and +{@link android.support.v4.content.LocalBroadcastManager} to +provide a cleaner implementation of broadcasts that don't need to go outside +of an app. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java new file mode 100644 index 00000000000..5c279236d50 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.content.pm; + +/** + * Helper for accessing features in {@link android.content.pm.ActivityInfo} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ActivityInfoCompat { + + private ActivityInfoCompat() { + /* Hide constructor */ + } + + /** + * Bit in ActivityInfo#configChanges that indicates that the + * activity can itself handle the ui mode. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_UI_MODE = 0x0200; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html b/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html new file mode 100644 index 00000000000..da850bda667 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html @@ -0,0 +1,6 @@ + + +Support android.content.pm classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java new file mode 100644 index 00000000000..252977b4a8a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java @@ -0,0 +1,93 @@ +/* + * 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. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.content.res.Resources} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ResourcesCompat { + /** + * Return a drawable object associated with a particular resource ID and + * styled for the specified theme. Various types of objects will be + * returned depending on the underlying resource -- for example, a solid + * color, PNG image, scalable image, etc. + *

+ * Prior to API level 21, the theme will not be applied and this method + * simply calls through to {@link Resources#getDrawable(int)}. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param theme The theme used to style the drawable attributes, may be + * {@code null}. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawable(Resources res, int id, Theme theme) + throws NotFoundException { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ResourcesCompatApi21.getDrawable(res, id, theme); + } else { + return res.getDrawable(id); + } + } + + + /** + * Return a drawable object associated with a particular resource ID for + * the given screen density in DPI and styled for the specified theme. + *

+ * Prior to API level 15, the theme and density will not be applied and + * this method simply calls through to {@link Resources#getDrawable(int)}. + *

+ * Prior to API level 21, the theme will not be applied and this method + * calls through to Resources.getDrawableForDensity(int, int). + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param density The desired screen density indicated by the resource as + * found in {@link android.util.DisplayMetrics}. + * @param theme The theme used to style the drawable attributes, may be + * {@code null}. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawableForDensity(Resources res, int id, int density, Theme theme) + throws NotFoundException { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ResourcesCompatApi21.getDrawableForDensity(res, id, density, theme); + } else if (version >= 15) { + return ResourcesCompatIcsMr1.getDrawableForDensity(res, id, density); + } else { + return res.getDrawable(id); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java new file mode 100644 index 00000000000..2272c02627e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; + +class ResourcesCompatApi21 { + public static Drawable getDrawable(Resources res, int id, Theme theme) + throws NotFoundException { + return res.getDrawable(id, theme); + } + + public static Drawable getDrawableForDensity(Resources res, int id, int density, Theme theme) + throws NotFoundException { + return res.getDrawableForDensity(id, density, theme); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java new file mode 100644 index 00000000000..8e14256bcab --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; + +class ResourcesCompatIcsMr1 { + public static Drawable getDrawableForDensity(Resources res, int id, int density) + throws NotFoundException { + return res.getDrawableForDensity(id, density); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java new file mode 100644 index 00000000000..9a825e80f93 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.database; + +import android.text.TextUtils; + +/** + * Helper for accessing features in {@link android.database.DatabaseUtils} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class DatabaseUtilsCompat { + + private DatabaseUtilsCompat() { + /* Hide constructor */ + } + + /** + * Concatenates two SQL WHERE clauses, handling empty or null values. + */ + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/database/package.html b/mobile/google-android-support-v4/src/android/support/v4/database/package.html new file mode 100644 index 00000000000..25ac59aafbb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/database/package.html @@ -0,0 +1,6 @@ + + +Support android.database classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java new file mode 100644 index 00000000000..5b1cb687d60 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java @@ -0,0 +1,111 @@ +/* + * 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. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Helper for accessing features in {@link android.graphics.Bitmap} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class BitmapCompat { + /** + * Interface for the full API. + */ + interface BitmapImpl { + public boolean hasMipMap(Bitmap bitmap); + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap); + public int getAllocationByteCount(Bitmap bitmap); + } + + static class BaseBitmapImpl implements BitmapImpl { + @Override + public boolean hasMipMap(Bitmap bitmap) { + return false; + } + + @Override + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + } + + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight(); + } + } + + static class HcMr1BitmapCompatImpl extends BaseBitmapImpl { + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return BitmapCompatHoneycombMr1.getAllocationByteCount(bitmap); + } + } + + static class JbMr2BitmapCompatImpl extends HcMr1BitmapCompatImpl { + @Override + public boolean hasMipMap(Bitmap bitmap){ + return BitmapCompatJellybeanMR2.hasMipMap(bitmap); + } + + @Override + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + BitmapCompatJellybeanMR2.setHasMipMap(bitmap, hasMipMap); + } + } + + static class KitKatBitmapCompatImpl extends JbMr2BitmapCompatImpl { + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return BitmapCompatKitKat.getAllocationByteCount(bitmap); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final BitmapImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatBitmapCompatImpl(); + } else if (version >= 18) { + IMPL = new JbMr2BitmapCompatImpl(); + } else if (version >= 12) { + IMPL = new HcMr1BitmapCompatImpl(); + } else { + IMPL = new BaseBitmapImpl(); + } + } + + public static boolean hasMipMap(Bitmap bitmap) { + return IMPL.hasMipMap(bitmap); + } + + public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + IMPL.setHasMipMap(bitmap, hasMipMap); + } + + /** + * Returns the size of the allocated memory used to store this bitmap's pixels in a backwards + * compatible way. + * + * @param bitmap the bitmap in which to return it's allocation size + * @return the allocation size in bytes + */ + public static int getAllocationByteCount(Bitmap bitmap) { + return IMPL.getAllocationByteCount(bitmap); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java new file mode 100644 index 00000000000..94358d5fffb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java @@ -0,0 +1,29 @@ +/* + * 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. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Implementation of BitmapCompat that can use Honeycomb MR1 APIs. + */ +class BitmapCompatHoneycombMr1 { + + static int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getByteCount(); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java new file mode 100644 index 00000000000..21f0d49b53b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +class BitmapCompatJellybeanMR2 { + public static boolean hasMipMap(Bitmap bitmap) { + return bitmap.hasMipMap(); + } + + public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + bitmap.setHasMipMap(hasMipMap); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java new file mode 100644 index 00000000000..2d4a66eda88 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * 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. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Implementation of BitmapCompat that can use KitKat APIs. + */ +class BitmapCompatKitKat { + + static int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getAllocationByteCount(); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java new file mode 100644 index 00000000000..aac809bceac --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java @@ -0,0 +1,297 @@ +/* + * Copyright 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.v4.graphics; + +import android.graphics.Color; + +/** + * A set of color-related utility methods, building upon those available in {@code Color}. + */ +public class ColorUtils { + + private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; + private static final int MIN_ALPHA_SEARCH_PRECISION = 10; + + private ColorUtils() {} + + /** + * Composite two potentially translucent colors over each other and returns the result. + */ + public static int compositeColors(int foreground, int background) { + int bgAlpha = Color.alpha(background); + int fgAlpha = Color.alpha(foreground); + int a = compositeAlpha(fgAlpha, bgAlpha); + + int r = compositeComponent(Color.red(foreground), fgAlpha, + Color.red(background), bgAlpha, a); + int g = compositeComponent(Color.green(foreground), fgAlpha, + Color.green(background), bgAlpha, a); + int b = compositeComponent(Color.blue(foreground), fgAlpha, + Color.blue(background), bgAlpha, a); + + return Color.argb(a, r, g, b); + } + + private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { + return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); + } + + private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { + if (a == 0) return 0; + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); + } + + /** + * Returns the luminance of a color. + * + * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + */ + public static double calculateLuminance(int color) { + double red = Color.red(color) / 255d; + red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); + + double green = Color.green(color) / 255d; + green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); + + double blue = Color.blue(color) / 255d; + blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); + + return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); + } + + /** + * Returns the contrast ratio between {@code foreground} and {@code background}. + * {@code background} must be opaque. + *

+ * Formula defined + * here. + */ + public static double calculateContrast(int foreground, int background) { + if (Color.alpha(background) != 255) { + throw new IllegalArgumentException("background can not be translucent"); + } + if (Color.alpha(foreground) < 255) { + // If the foreground is translucent, composite the foreground over the background + foreground = compositeColors(foreground, background); + } + + final double luminance1 = calculateLuminance(foreground) + 0.05; + final double luminance2 = calculateLuminance(background) + 0.05; + + // Now return the lighter luminance divided by the darker luminance + return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); + } + + /** + * Calculates the minimum alpha value which can be applied to {@code foreground} so that would + * have a contrast value of at least {@code minContrastRatio} when compared to + * {@code background}. + * + * @param foreground the foreground color. + * @param background the background color. Should be opaque. + * @param minContrastRatio the minimum contrast ratio. + * @return the alpha value in the range 0-255, or -1 if no value could be calculated. + */ + public static int calculateMinimumAlpha(int foreground, int background, + float minContrastRatio) { + if (Color.alpha(background) != 255) { + throw new IllegalArgumentException("background can not be translucent"); + } + + // First lets check that a fully opaque foreground has sufficient contrast + int testForeground = setAlphaComponent(foreground, 255); + double testRatio = calculateContrast(testForeground, background); + if (testRatio < minContrastRatio) { + // Fully opaque foreground does not have sufficient contrast, return error + return -1; + } + + // Binary search to find a value with the minimum value which provides sufficient contrast + int numIterations = 0; + int minAlpha = 0; + int maxAlpha = 255; + + while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && + (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { + final int testAlpha = (minAlpha + maxAlpha) / 2; + + testForeground = setAlphaComponent(foreground, testAlpha); + testRatio = calculateContrast(testForeground, background); + + if (testRatio < minContrastRatio) { + minAlpha = testAlpha; + } else { + maxAlpha = testAlpha; + } + + numIterations++; + } + + // Conservatively return the max of the range of possible alphas, which is known to pass. + return maxAlpha; + } + + /** + * Convert RGB components to HSL (hue-saturation-lightness). + *

    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param hsl 3 element array which holds the resulting HSL components. + */ + public static void RGBToHSL(int r, int g, int b, float[] hsl) { + final float rf = r / 255f; + final float gf = g / 255f; + final float bf = b / 255f; + + final float max = Math.max(rf, Math.max(gf, bf)); + final float min = Math.min(rf, Math.min(gf, bf)); + final float deltaMaxMin = max - min; + + float h, s; + float l = (max + min) / 2f; + + if (max == min) { + // Monochromatic + h = s = 0f; + } else { + if (max == rf) { + h = ((gf - bf) / deltaMaxMin) % 6f; + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2f; + } else { + h = ((rf - gf) / deltaMaxMin) + 4f; + } + + s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); + } + + h = (h * 60f) % 360f; + if (h < 0) { + h += 360f; + } + + hsl[0] = constrain(h, 0f, 360f); + hsl[1] = constrain(s, 0f, 1f); + hsl[2] = constrain(l, 0f, 1f); + } + + /** + * Convert the ARGB color to its HSL (hue-saturation-lightness) components. + *
    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored. + * @param hsl 3 element array which holds the resulting HSL components. + */ + public static void colorToHSL(int color, float[] hsl) { + RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl); + } + + /** + * Convert HSL (hue-saturation-lightness) components to a RGB color. + *
    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * If hsv values are out of range, they are pinned. + * + * @param hsl 3 element array which holds the input HSL components. + * @return the resulting RGB color + */ + public static int HSLToColor(float[] hsl) { + final float h = hsl[0]; + final float s = hsl[1]; + final float l = hsl[2]; + + final float c = (1f - Math.abs(2 * l - 1f)) * s; + final float m = l - 0.5f * c; + final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); + + final int hueSegment = (int) h / 60; + + int r = 0, g = 0, b = 0; + + switch (hueSegment) { + case 0: + r = Math.round(255 * (c + m)); + g = Math.round(255 * (x + m)); + b = Math.round(255 * m); + break; + case 1: + r = Math.round(255 * (x + m)); + g = Math.round(255 * (c + m)); + b = Math.round(255 * m); + break; + case 2: + r = Math.round(255 * m); + g = Math.round(255 * (c + m)); + b = Math.round(255 * (x + m)); + break; + case 3: + r = Math.round(255 * m); + g = Math.round(255 * (x + m)); + b = Math.round(255 * (c + m)); + break; + case 4: + r = Math.round(255 * (x + m)); + g = Math.round(255 * m); + b = Math.round(255 * (c + m)); + break; + case 5: + case 6: + r = Math.round(255 * (c + m)); + g = Math.round(255 * m); + b = Math.round(255 * (x + m)); + break; + } + + r = constrain(r, 0, 255); + g = constrain(g, 0, 255); + b = constrain(b, 0, 255); + + return Color.rgb(r, g, b); + } + + /** + * Set the alpha component of {@code color} to be {@code alpha}. + */ + public static int setAlphaComponent(int color, int alpha) { + if (alpha < 0 || alpha > 255) { + throw new IllegalArgumentException("alpha must be between 0 and 255."); + } + return (color & 0x00ffffff) | (alpha << 24); + } + + private static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java new file mode 100644 index 00000000000..99c762f13b5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java @@ -0,0 +1,318 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Helper for accessing features in {@link android.graphics.drawable.Drawable} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class DrawableCompat { + /** + * Interface for the full API. + */ + interface DrawableImpl { + void jumpToCurrentState(Drawable drawable); + void setAutoMirrored(Drawable drawable, boolean mirrored); + boolean isAutoMirrored(Drawable drawable); + void setHotspot(Drawable drawable, float x, float y); + void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom); + void setTint(Drawable drawable, int tint); + void setTintList(Drawable drawable, ColorStateList tint); + void setTintMode(Drawable drawable, PorterDuff.Mode tintMode); + Drawable wrap(Drawable drawable); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseDrawableImpl implements DrawableImpl { + @Override + public void jumpToCurrentState(Drawable drawable) { + } + + @Override + public void setAutoMirrored(Drawable drawable, boolean mirrored) { + } + + @Override + public boolean isAutoMirrored(Drawable drawable) { + return false; + } + + @Override + public void setHotspot(Drawable drawable, float x, float y) { + } + + @Override + public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { + } + + @Override + public void setTint(Drawable drawable, int tint) { + DrawableCompatBase.setTint(drawable, tint); + } + + @Override + public void setTintList(Drawable drawable, ColorStateList tint) { + DrawableCompatBase.setTintList(drawable, tint); + } + + @Override + public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + DrawableCompatBase.setTintMode(drawable, tintMode); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatBase.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombDrawableImpl extends BaseDrawableImpl { + @Override + public void jumpToCurrentState(Drawable drawable) { + DrawableCompatHoneycomb.jumpToCurrentState(drawable); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatHoneycomb.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatDrawableImpl extends HoneycombDrawableImpl { + @Override + public void setAutoMirrored(Drawable drawable, boolean mirrored) { + DrawableCompatKitKat.setAutoMirrored(drawable, mirrored); + } + + @Override + public boolean isAutoMirrored(Drawable drawable) { + return DrawableCompatKitKat.isAutoMirrored(drawable); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatKitKat.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least L APIs. + */ + static class LollipopDrawableImpl extends KitKatDrawableImpl { + @Override + public void setHotspot(Drawable drawable, float x, float y) { + DrawableCompatLollipop.setHotspot(drawable, x, y); + } + + @Override + public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { + DrawableCompatLollipop.setHotspotBounds(drawable, left, top, right, bottom); + } + + @Override + public void setTint(Drawable drawable, int tint) { + DrawableCompatLollipop.setTint(drawable, tint); + } + + @Override + public void setTintList(Drawable drawable, ColorStateList tint) { + DrawableCompatLollipop.setTintList(drawable, tint); + } + + @Override + public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + DrawableCompatLollipop.setTintMode(drawable, tintMode); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatLollipop.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least L APIs. + */ + static class LollipopMr1DrawableImpl extends LollipopDrawableImpl { + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatApi22.wrapForTinting(drawable); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final DrawableImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 22) { + IMPL = new LollipopMr1DrawableImpl(); + } else if (version >= 21) { + IMPL = new LollipopDrawableImpl(); + } else if (version >= 19) { + IMPL = new KitKatDrawableImpl(); + } else if (version >= 11) { + IMPL = new HoneycombDrawableImpl(); + } else { + IMPL = new BaseDrawableImpl(); + } + } + + /** + * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} + * device this method does nothing. + * + * @param drawable The Drawable against which to invoke the method. + */ + public static void jumpToCurrentState(Drawable drawable) { + IMPL.jumpToCurrentState(drawable); + } + + /** + * Set whether this Drawable is automatically mirrored when its layout + * direction is RTL (right-to left). See + * {@link android.util.LayoutDirection}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device + * this method does nothing. + * + * @param drawable The Drawable against which to invoke the method. + * @param mirrored Set to true if the Drawable should be mirrored, false if + * not. + */ + public static void setAutoMirrored(Drawable drawable, boolean mirrored) { + IMPL.setAutoMirrored(drawable, mirrored); + } + + /** + * Tells if this Drawable will be automatically mirrored when its layout + * direction is RTL right-to-left. See {@link android.util.LayoutDirection}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device + * this method returns false. + * + * @param drawable The Drawable against which to invoke the method. + * @return boolean Returns true if this Drawable will be automatically + * mirrored. + */ + public static boolean isAutoMirrored(Drawable drawable) { + return IMPL.isAutoMirrored(drawable); + } + + /** + * Specifies the hotspot's location within the drawable. + * + * @param drawable The Drawable against which to invoke the method. + * @param x The X coordinate of the center of the hotspot + * @param y The Y coordinate of the center of the hotspot + */ + public static void setHotspot(Drawable drawable, float x, float y) { + IMPL.setHotspot(drawable, x, y); + } + + /** + * Sets the bounds to which the hotspot is constrained, if they should be + * different from the drawable bounds. + * + * @param drawable The Drawable against which to invoke the method. + */ + public static void setHotspotBounds(Drawable drawable, int left, int top, + int right, int bottom) { + IMPL.setHotspotBounds(drawable, left, top, right, bottom); + } + + /** + * Specifies a tint for {@code drawable}. + * + * @param drawable The Drawable against which to invoke the method. + * @param tint Color to use for tinting this drawable + */ + public static void setTint(Drawable drawable, int tint) { + IMPL.setTint(drawable, tint); + } + + /** + * Specifies a tint for {@code drawable} as a color state list. + * + * @param drawable The Drawable against which to invoke the method. + * @param tint Color state list to use for tinting this drawable, or null to clear the tint + */ + public static void setTintList(Drawable drawable, ColorStateList tint) { + IMPL.setTintList(drawable, tint); + } + + /** + * Specifies a tint blending mode for {@code drawable}. + * + * @param drawable The Drawable against which to invoke the method. + * @param tintMode A Porter-Duff blending mode + */ + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + IMPL.setTintMode(drawable, tintMode); + } + + /** + * Potentially wrap {@code drawable} so that it may be used for tinting across the + * different API levels, via the tinting methods in this class. + *

+ * If you need to get hold of the original {@link android.graphics.drawable.Drawable} again, + * you can use the value returned from {@link #unwrap(Drawable)}. + * + * @param drawable The Drawable to process + * @return A drawable capable of being tinted across all API levels. + * + * @see #setTint(Drawable, int) + * @see #setTintList(Drawable, ColorStateList) + * @see #setTintMode(Drawable, PorterDuff.Mode) + * @see #unwrap(Drawable) + */ + public static Drawable wrap(Drawable drawable) { + return IMPL.wrap(drawable); + } + + /** + * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If + * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then + * {@code drawable} is returned as-is. + * + * @param drawable The drawable to unwrap + * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped. + * + * @see #wrap(Drawable) + */ + public static T unwrap(Drawable drawable) { + if (drawable instanceof DrawableWrapper) { + return (T) ((DrawableWrapper) drawable).getWrappedDrawable(); + } + return (T) drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java new file mode 100644 index 00000000000..bfd2bea1228 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java @@ -0,0 +1,31 @@ +/* + * 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.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call Lollipop-MR1 APIs. + */ +class DrawableCompatApi22 { + + public static Drawable wrapForTinting(Drawable drawable) { + // We don't need to wrap anything in Lollipop-MR1 + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java new file mode 100644 index 00000000000..48096185275 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java @@ -0,0 +1,53 @@ +/* + * 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.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Base implementation of drawable compatibility. + */ +class DrawableCompatBase { + + public static void setTint(Drawable drawable, int tint) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTint(tint); + } + } + + public static void setTintList(Drawable drawable, ColorStateList tint) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTintList(tint); + } + } + + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTintMode(tintMode); + } + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperDonut)) { + return new DrawableWrapperDonut(drawable); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java new file mode 100644 index 00000000000..def23ed5ce7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call Honeycomb APIs. + */ +class DrawableCompatHoneycomb { + + public static void jumpToCurrentState(Drawable drawable) { + drawable.jumpToCurrentState(); + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperHoneycomb)) { + return new DrawableWrapperHoneycomb(drawable); + } + return drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java new file mode 100644 index 00000000000..b42c86b91fb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java @@ -0,0 +1,41 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call KitKat APIs. + */ +class DrawableCompatKitKat { + public static void setAutoMirrored(Drawable drawable, boolean mirrored) { + drawable.setAutoMirrored(mirrored); + } + + public static boolean isAutoMirrored(Drawable drawable) { + return drawable.isAutoMirrored(); + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperKitKat)) { + return new DrawableWrapperKitKat(drawable); + } + return drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java new file mode 100644 index 00000000000..e5388b9ea8b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java @@ -0,0 +1,80 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; + +/** + * Implementation of drawable compatibility that can call L APIs. + */ +class DrawableCompatLollipop { + + public static void setHotspot(Drawable drawable, float x, float y) { + drawable.setHotspot(x, y); + } + + public static void setHotspotBounds(Drawable drawable, int left, int top, + int right, int bottom) { + drawable.setHotspotBounds( left, top, right, bottom); + } + + public static void setTint(Drawable drawable, int tint) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTint(drawable, tint); + } else { + // Else, we'll use the framework API + drawable.setTint(tint); + } + } + + public static void setTintList(Drawable drawable, ColorStateList tint) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTintList(drawable, tint); + } else { + // Else, we'll use the framework API + drawable.setTintList(tint); + } + } + + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTintMode(drawable, tintMode); + } else { + // Else, we'll use the framework API + drawable.setTintMode(tintMode); + } + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (drawable instanceof GradientDrawable) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + return new DrawableWrapperLollipop(drawable); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java new file mode 100644 index 00000000000..1073f344c60 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java @@ -0,0 +1,41 @@ +/* + * 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.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Interface which allows a {@link android.graphics.drawable.Drawable} to receive tinting calls from + * {@code DrawableCompat}. + * + * @hide + */ +public interface DrawableWrapper { + + void setTint(int tint); + + void setTintList(ColorStateList tint); + + void setTintMode(PorterDuff.Mode tintMode); + + Drawable getWrappedDrawable(); + + void setWrappedDrawable(Drawable drawable); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java new file mode 100644 index 00000000000..e3672c8266e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java @@ -0,0 +1,245 @@ +/* + * 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.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; + +/** + * Drawable which delegates all calls to it's wrapped {@link android.graphics.drawable.Drawable}. + *

+ * Also allows backward compatible tinting via a color or {@link ColorStateList}. + * This functionality is accessed via static methods in {@code DrawableCompat}. + */ +class DrawableWrapperDonut extends Drawable implements Drawable.Callback, DrawableWrapper { + + static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; + + private ColorStateList mTintList; + private PorterDuff.Mode mTintMode = DEFAULT_MODE; + + private int mCurrentColor; + private PorterDuff.Mode mCurrentMode; + private boolean mColorFilterSet; + + Drawable mDrawable; + + DrawableWrapperDonut(Drawable drawable) { + setWrappedDrawable(drawable); + } + + @Override + public void draw(Canvas canvas) { + mDrawable.draw(canvas); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mDrawable.setBounds(bounds); + } + + @Override + public void setChangingConfigurations(int configs) { + mDrawable.setChangingConfigurations(configs); + } + + @Override + public int getChangingConfigurations() { + return mDrawable.getChangingConfigurations(); + } + + @Override + public void setDither(boolean dither) { + mDrawable.setDither(dither); + } + + @Override + public void setFilterBitmap(boolean filter) { + mDrawable.setFilterBitmap(filter); + } + + @Override + public void setAlpha(int alpha) { + mDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mDrawable.setColorFilter(cf); + } + + @Override + public boolean isStateful() { + return (mTintList != null && mTintList.isStateful()) || mDrawable.isStateful(); + } + + @Override + public boolean setState(final int[] stateSet) { + boolean handled = mDrawable.setState(stateSet); + handled = updateTint(stateSet) || handled; + return handled; + } + + @Override + public int[] getState() { + return mDrawable.getState(); + } + + @Override + public Drawable getCurrent() { + return mDrawable.getCurrent(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return super.setVisible(visible, restart) || mDrawable.setVisible(visible, restart); + } + + @Override + public int getOpacity() { + return mDrawable.getOpacity(); + } + + @Override + public Region getTransparentRegion() { + return mDrawable.getTransparentRegion(); + } + + @Override + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + @Override + public int getMinimumWidth() { + return mDrawable.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return mDrawable.getMinimumHeight(); + } + + @Override + public boolean getPadding(Rect padding) { + return mDrawable.getPadding(padding); + } + + @Override + public Drawable mutate() { + Drawable wrapped = mDrawable; + Drawable mutated = wrapped.mutate(); + if (mutated != wrapped) { + // If mutate() returned a new instance, update our reference + setWrappedDrawable(mutated); + } + // We return ourselves, since only the wrapped drawable needs to mutate + return this; + } + + /** + * {@inheritDoc} + */ + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + /** + * {@inheritDoc} + */ + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + /** + * {@inheritDoc} + */ + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + protected boolean onLevelChange(int level) { + return mDrawable.setLevel(level); + } + + @Override + public void setTint(int tint) { + setTintList(ColorStateList.valueOf(tint)); + } + + @Override + public void setTintList(ColorStateList tint) { + mTintList = tint; + updateTint(getState()); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + mTintMode = tintMode; + updateTint(getState()); + } + + private boolean updateTint(int[] state) { + if (mTintList != null && mTintMode != null) { + final int color = mTintList.getColorForState(state, mTintList.getDefaultColor()); + final PorterDuff.Mode mode = mTintMode; + if (!mColorFilterSet || color != mCurrentColor || mode != mCurrentMode) { + setColorFilter(color, mode); + mCurrentColor = color; + mCurrentMode = mode; + mColorFilterSet = true; + return true; + } + } + return false; + } + + /** + * Returns the wrapped {@link Drawable} + */ + public Drawable getWrappedDrawable() { + return mDrawable; + } + + /** + * Sets the current wrapped {@link Drawable} + */ + public void setWrappedDrawable(Drawable drawable) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + + mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + // Invalidate ourselves + invalidateSelf(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java new file mode 100644 index 00000000000..f9fd7d84fae --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java @@ -0,0 +1,31 @@ +/* + * 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.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +class DrawableWrapperHoneycomb extends DrawableWrapperDonut { + + DrawableWrapperHoneycomb(Drawable drawable) { + super(drawable); + } + + @Override + public void jumpToCurrentState() { + mDrawable.jumpToCurrentState(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java new file mode 100644 index 00000000000..dc70c6eeb3f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java @@ -0,0 +1,36 @@ +/* + * 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.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +class DrawableWrapperKitKat extends DrawableWrapperHoneycomb { + + DrawableWrapperKitKat(Drawable drawable) { + super(drawable); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mDrawable.setAutoMirrored(mirrored); + } + + @Override + public boolean isAutoMirrored() { + return mDrawable.isAutoMirrored(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java new file mode 100644 index 00000000000..1f15040056c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java @@ -0,0 +1,59 @@ +/* + * 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.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +class DrawableWrapperLollipop extends DrawableWrapperKitKat { + + DrawableWrapperLollipop(Drawable drawable) { + super(drawable); + } + + @Override + public void setHotspot(float x, float y) { + mDrawable.setHotspot(x, y); + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + mDrawable.setHotspotBounds(left, top, right, bottom); + } + + @Override + public void getOutline(Outline outline) { + mDrawable.getOutline(outline); + } + + @Override + public void applyTheme(Resources.Theme t) { + mDrawable.applyTheme(t); + } + + @Override + public boolean canApplyTheme() { + return mDrawable.canApplyTheme(); + } + + @Override + public Rect getDirtyBounds() { + return mDrawable.getDirtyBounds(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java new file mode 100644 index 00000000000..7231a382124 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java @@ -0,0 +1,327 @@ +/* + * 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. + */ +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.view.Gravity; + +/** + * A Drawable that wraps a bitmap and can be drawn with rounded corners. You can create a + * RoundedBitmapDrawable from a file path, an input stream, or from a + * {@link android.graphics.Bitmap} object. + *

+ * Also see the {@link android.graphics.Bitmap} class, which handles the management and + * transformation of raw bitmap graphics, and should be used when drawing to a + * {@link android.graphics.Canvas}. + *

+ */ +public abstract class RoundedBitmapDrawable extends Drawable { + private static final int DEFAULT_PAINT_FLAGS = + Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; + Bitmap mBitmap; + private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; + private int mGravity = Gravity.FILL; + private Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); + private BitmapShader mBitmapShader; + private float mCornerRadius; + + final Rect mDstRect = new Rect(); // Gravity.apply() sets this + final RectF mDstRectF = new RectF(); + + private boolean mApplyGravity = true; + + // These are scaled to match the target density. + private int mBitmapWidth; + private int mBitmapHeight; + + /** + * Returns the paint used to render this drawable. + */ + public final Paint getPaint() { + return mPaint; + } + + /** + * Returns the bitmap used by this drawable to render. May be null. + */ + public final Bitmap getBitmap() { + return mBitmap; + } + + private void computeBitmapSize() { + mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity); + mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity); + } + + /** + * Set the density scale at which this drawable will be rendered. This + * method assumes the drawable will be rendered at the same density as the + * specified canvas. + * + * @param canvas The Canvas from which the density scale must be obtained. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(Canvas canvas) { + setTargetDensity(canvas.getDensity()); + } + + /** + * Set the density scale at which this drawable will be rendered. + * + * @param metrics The DisplayMetrics indicating the density scale for this drawable. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(DisplayMetrics metrics) { + setTargetDensity(metrics.densityDpi); + } + + /** + * Set the density at which this drawable will be rendered. + * + * @param density The density scale for this drawable. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(int density) { + if (mTargetDensity != density) { + mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; + if (mBitmap != null) { + computeBitmapSize(); + } + invalidateSelf(); + } + } + + /** + * Get the gravity used to position/stretch the bitmap within its bounds. + * + * @return the gravity applied to the bitmap + * + * @see android.view.Gravity + */ + public int getGravity() { + return mGravity; + } + + /** + * Set the gravity used to position/stretch the bitmap within its bounds. + * + * @param gravity the gravity + * + * @see android.view.Gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + mGravity = gravity; + mApplyGravity = true; + invalidateSelf(); + } + } + + /** + * Enables or disables the mipmap hint for this drawable's bitmap. + * See {@link Bitmap#setHasMipMap(boolean)} for more information. + * + * If the bitmap is null, or the current API version does not support setting a mipmap hint, + * calling this method has no effect. + * + * @param mipMap True if the bitmap should use mipmaps, false otherwise. + * + * @see #hasMipMap() + */ + public void setMipMap(boolean mipMap) { + throw new UnsupportedOperationException(); // must be overridden in subclasses + } + + /** + * Indicates whether the mipmap hint is enabled on this drawable's bitmap. + * + * @return True if the mipmap hint is set, false otherwise. If the bitmap + * is null, this method always returns false. + * + * @see #setMipMap(boolean) + */ + public boolean hasMipMap() { + throw new UnsupportedOperationException(); // must be overridden in subclasses + } + + /** + * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects + * the edges of the bitmap only so it applies only when the drawable is rotated. + * + * @param aa True if the bitmap should be anti-aliased, false otherwise. + * + * @see #hasAntiAlias() + */ + public void setAntiAlias(boolean aa) { + mPaint.setAntiAlias(aa); + invalidateSelf(); + } + + /** + * Indicates whether anti-aliasing is enabled for this drawable. + * + * @return True if anti-aliasing is enabled, false otherwise. + * + * @see #setAntiAlias(boolean) + */ + public boolean hasAntiAlias() { + return mPaint.isAntiAlias(); + } + + @Override + public void setFilterBitmap(boolean filter) { + mPaint.setFilterBitmap(filter); + invalidateSelf(); + } + + @Override + public void setDither(boolean dither) { + mPaint.setDither(dither); + invalidateSelf(); + } + + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + throw new UnsupportedOperationException(); + } + + void updateDstRect() { + if (mApplyGravity) { + gravityCompatApply(mGravity, mBitmapWidth, mBitmapHeight, + getBounds(), mDstRect); + mDstRectF.set(mDstRect); + mApplyGravity = false; + } + } + + @Override + public void draw(Canvas canvas) { + final Bitmap bitmap = mBitmap; + if (bitmap == null) { + return; + } + + updateDstRect(); + + final Paint paint = mPaint; + final Shader shader = paint.getShader(); + if (shader == null) { + canvas.drawBitmap(bitmap, null, mDstRect, paint); + } else { + canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint); + } + } + + @Override + public void setAlpha(int alpha) { + final int oldAlpha = mPaint.getAlpha(); + if (alpha != oldAlpha) { + mPaint.setAlpha(alpha); + invalidateSelf(); + } + } + + public int getAlpha() { + return mPaint.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + invalidateSelf(); + } + + public ColorFilter getColorFilter() { + return mPaint.getColorFilter(); + } + + /** + * Sets the corner radius to be applied when drawing the bitmap. + */ + public void setCornerRadius(float cornerRadius) { + if (isGreaterThanZero(cornerRadius)) { + mPaint.setShader(mBitmapShader); + } else { + mPaint.setShader(null); + } + mCornerRadius = cornerRadius; + } + + /** + * @return The corner radius applied when drawing the bitmap. + */ + public float getCornerRadius() { + return mCornerRadius; + } + + @Override + public int getIntrinsicWidth() { + return mBitmapWidth; + } + + @Override + public int getIntrinsicHeight() { + return mBitmapHeight; + } + + @Override + public int getOpacity() { + if (mGravity != Gravity.FILL) { + return PixelFormat.TRANSLUCENT; + } + Bitmap bm = mBitmap; + return (bm == null + || bm.hasAlpha() + || mPaint.getAlpha() < 255 + || isGreaterThanZero(mCornerRadius)) + ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + RoundedBitmapDrawable(Resources res, Bitmap bitmap) { + if (res != null) { + mTargetDensity = res.getDisplayMetrics().densityDpi; + } + + mBitmap = bitmap; + if (mBitmap != null) { + computeBitmapSize(); + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + } else { + mBitmapWidth = mBitmapHeight = -1; + } + } + + private static boolean isGreaterThanZero(float toCompare) { + return Float.compare(toCompare, +0.0f) > 0; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java new file mode 100644 index 00000000000..081feea4e74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Outline; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; + +class RoundedBitmapDrawable21 extends RoundedBitmapDrawable { + protected RoundedBitmapDrawable21(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void getOutline(Outline outline) { + updateDstRect(); + outline.setRoundRect(mDstRect, getCornerRadius()); + } + + @Override + public void setMipMap(boolean mipMap) { + if (mBitmap != null) { + mBitmap.setHasMipMap(mipMap); + invalidateSelf(); + } + } + + @Override + public boolean hasMipMap() { + return mBitmap != null && mBitmap.hasMipMap(); + } + + @Override + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + Gravity.apply(gravity, bitmapWidth, bitmapHeight, + bounds, outRect, View.LAYOUT_DIRECTION_LTR); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java new file mode 100644 index 00000000000..0376c988968 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java @@ -0,0 +1,98 @@ +/* + * 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. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.os.Build; +import android.support.v4.graphics.BitmapCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.util.Log; + +/** + * Constructs {@link RoundedBitmapDrawable RoundedBitmapDrawable} objects, + * either from Bitmaps directly, or from streams and files. + */ +public class RoundedBitmapDrawableFactory { + private static final String TAG = "RoundedBitmapDrawableFactory"; + + private static class DefaultRoundedBitmapDrawable extends RoundedBitmapDrawable { + DefaultRoundedBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void setMipMap(boolean mipMap) { + if (mBitmap != null) { + BitmapCompat.setHasMipMap(mBitmap, mipMap); + invalidateSelf(); + } + } + + @Override + public boolean hasMipMap() { + return mBitmap != null && BitmapCompat.hasMipMap(mBitmap); + } + + @Override + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + GravityCompat.apply(gravity, bitmapWidth, bitmapHeight, + bounds, outRect, ViewCompat.LAYOUT_DIRECTION_LTR); + } + } + + /** + * Returns a new drawable by creating it from a bitmap, setting initial target density based on + * the display metrics of the resources. + */ + public static RoundedBitmapDrawable create(Resources res, Bitmap bitmap) { + if (Build.VERSION.SDK_INT >= 21) { + return new RoundedBitmapDrawable21(res, bitmap); + } + return new DefaultRoundedBitmapDrawable(res, bitmap); + } + + /** + * Returns a new drawable, creating it by opening a given file path and decoding the bitmap. + */ + public static RoundedBitmapDrawable create(Resources res, + String filepath) { + final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeFile(filepath)); + if (drawable.getBitmap() == null) { + Log.w(TAG, "BitmapDrawable cannot decode " + filepath); + } + return drawable; + } + + + /** + * Returns a new drawable, creating it by decoding a bitmap from the given input stream. + */ + public static RoundedBitmapDrawable create(Resources res, + java.io.InputStream is) { + final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeStream(is)); + if (drawable.getBitmap() == null) { + Log.w(TAG, "BitmapDrawable cannot decode " + is); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java new file mode 100644 index 00000000000..177c40adb03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java @@ -0,0 +1,154 @@ +/* + * 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. + */ + +package android.support.v4.hardware.display; + +import android.content.Context; +import android.view.Display; +import android.view.WindowManager; + +import java.util.WeakHashMap; + +/** + * Helper for accessing features in {@link android.hardware.display.DisplayManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public abstract class DisplayManagerCompat { + private static final WeakHashMap sInstances = + new WeakHashMap(); + + /** + * Display category: Presentation displays. + *

+ * This category can be used to identify secondary displays that are suitable for + * use as presentation displays. + *

+ * + * @see android.app.Presentation for information about presenting content + * on secondary displays. + * @see #getDisplays(String) + */ + public static final String DISPLAY_CATEGORY_PRESENTATION = + "android.hardware.display.category.PRESENTATION"; + + DisplayManagerCompat() { + } + + /** + * Gets an instance of the display manager given the context. + */ + public static DisplayManagerCompat getInstance(Context context) { + synchronized (sInstances) { + DisplayManagerCompat instance = sInstances.get(context); + if (instance == null) { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 17) { + instance = new JellybeanMr1Impl(context); + } else { + instance = new LegacyImpl(context); + } + sInstances.put(context, instance); + } + return instance; + } + } + + /** + * Gets information about a logical display. + * + * The display metrics may be adjusted to provide compatibility + * for legacy applications. + * + * @param displayId The logical display id. + * @return The display object, or null if there is no valid display with the given id. + */ + public abstract Display getDisplay(int displayId); + + /** + * Gets all currently valid logical displays. + * + * @return An array containing all displays. + */ + public abstract Display[] getDisplays(); + + /** + * Gets all currently valid logical displays of the specified category. + *

+ * When there are multiple displays in a category the returned displays are sorted + * of preference. For example, if the requested category is + * {@link #DISPLAY_CATEGORY_PRESENTATION} and there are multiple presentation displays + * then the displays are sorted so that the first display in the returned array + * is the most preferred presentation display. The application may simply + * use the first display or allow the user to choose. + *

+ * + * @param category The requested display category or null to return all displays. + * @return An array containing all displays sorted by order of preference. + * + * @see #DISPLAY_CATEGORY_PRESENTATION + */ + public abstract Display[] getDisplays(String category); + + private static class LegacyImpl extends DisplayManagerCompat { + private final WindowManager mWindowManager; + + public LegacyImpl(Context context) { + mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + } + + @Override + public Display getDisplay(int displayId) { + Display display = mWindowManager.getDefaultDisplay(); + if (display.getDisplayId() == displayId) { + return display; + } + return null; + } + + @Override + public Display[] getDisplays() { + return new Display[] { mWindowManager.getDefaultDisplay() }; + } + + @Override + public Display[] getDisplays(String category) { + return category == null ? getDisplays() : new Display[0]; + } + } + + private static class JellybeanMr1Impl extends DisplayManagerCompat { + private final Object mDisplayManagerObj; + + public JellybeanMr1Impl(Context context) { + mDisplayManagerObj = DisplayManagerJellybeanMr1.getDisplayManager(context); + } + + @Override + public Display getDisplay(int displayId) { + return DisplayManagerJellybeanMr1.getDisplay(mDisplayManagerObj, displayId); + } + + @Override + public Display[] getDisplays() { + return DisplayManagerJellybeanMr1.getDisplays(mDisplayManagerObj); + } + + @Override + public Display[] getDisplays(String category) { + return DisplayManagerJellybeanMr1.getDisplays(mDisplayManagerObj, category); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java new file mode 100644 index 00000000000..b44a2e8f780 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package android.support.v4.hardware.display; + +import android.content.Context; +import android.view.Display; + +final class DisplayManagerJellybeanMr1 { + public static Object getDisplayManager(Context context) { + return context.getSystemService(Context.DISPLAY_SERVICE); + } + + public static Display getDisplay(Object displayManagerObj, int displayId) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplay(displayId); + } + + public static Display[] getDisplays(Object displayManagerObj) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplays(); + } + + public static Display[] getDisplays(Object displayManagerObj, String category) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplays(category); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java new file mode 100644 index 00000000000..2a04cd83b91 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package android.support.v4.internal.view; + +/** + * Interface for managing the items in a menu. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.Menu + * @hide + */ +public interface SupportMenu extends android.view.Menu { + + /** + * This is the part of an order integer that the user can provide. + * + * @hide + */ + static final int USER_MASK = 0x0000ffff; + /** + * Bit shift of the user portion of the order integer. + * + * @hide + */ + static final int USER_SHIFT = 0; + + /** + * This is the part of an order integer that supplies the category of the item. + * + * @hide + */ + static final int CATEGORY_MASK = 0xffff0000; + /** + * Bit shift of the category portion of the order integer. + * + * @hide + */ + static final int CATEGORY_SHIFT = 16; +} + diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java new file mode 100644 index 00000000000..0a1bccbb2c6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java @@ -0,0 +1,204 @@ +/* + * 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. + */ + +package android.support.v4.internal.view; + +import android.support.v4.view.ActionProvider; +import android.support.v4.view.MenuItemCompat; +import android.view.MenuItem; +import android.view.View; + +/** + * Interface for direct access to a previously created menu item. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.MenuItem + * @hide + */ +public interface SupportMenuItem extends android.view.MenuItem { + /* + * These should be kept in sync with attrs.xml enum constants for showAsAction + */ + /** + * Never show this item as a button in an Action Bar. + */ + public static final int SHOW_AS_ACTION_NEVER = 0; + /** + * Show this item as a button in an Action Bar if the system decides there is room for it. + */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + /** + * Always show this item as a button in an Action Bar. + * Use sparingly! If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with smaller screens. + * A good rule of thumb is to have no more than 2 items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + + /** + * When this item is in the action bar, always show it with a text label even if it also has an + * icon specified. + */ + public static final int SHOW_AS_ACTION_WITH_TEXT = 4; + + /** + * This item's action view collapses to a normal menu item. When expanded, the action view + * temporarily takes over a larger segment of its container. + */ + public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; + + /** + * Sets how this item should display in the presence of an Action Bar. The parameter actionEnum + * is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or + * {@link #SHOW_AS_ACTION_NEVER} should be used, and you may optionally OR the value with {@link + * #SHOW_AS_ACTION_WITH_TEXT}. SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as + * an action, it should be shown with a text label. + * + * @param actionEnum How the item should display. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link + * #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER}. + * SHOW_AS_ACTION_NEVER is the default. + * @see android.app.ActionBar + * @see #setActionView(View) + */ + public void setShowAsAction(int actionEnum); + + /** + * Sets how this item should display in the presence of an Action Bar. + * The parameter actionEnum is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS}, + * {@link #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER} should + * be used, and you may optionally OR the value with {@link #SHOW_AS_ACTION_WITH_TEXT}. + * SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as an action, + * it should be shown with a text label. + * + *

Note: This method differs from {@link #setShowAsAction(int)} only in that it + * returns the current MenuItem instance for call chaining. + * + * @param actionEnum How the item should display. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link + * #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER}. + * SHOW_AS_ACTION_NEVER is the default. + * @return This MenuItem instance for call chaining. + * @see android.app.ActionBar + * @see #setActionView(View) + */ + public MenuItem setShowAsActionFlags(int actionEnum); + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + *

Note: Setting an action view overrides the action provider + * provider set via {@link #setSupportActionProvider(android.support.v4.view.ActionProvider)}.

+ * + * @param view View to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * @see #setShowAsAction(int) + */ + public MenuItem setActionView(View view); + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + *

Note: Setting an action view overrides the action provider + * provider set via {@link #setSupportActionProvider(android.support.v4.view.ActionProvider)}.

+ * + * @param resId Layout resource to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * @see #setShowAsAction(int) + */ + public MenuItem setActionView(int resId); + + /** + * Returns the currently set action view for this menu item. + * + * @return This item's action view + * @see #setActionView(View) + * @see #setShowAsAction(int) + */ + public View getActionView(); + + /** + * Sets the {@link android.support.v4.view.ActionProvider} responsible for creating an action view if + * the item is placed on the action bar. The provider also provides a default + * action invoked if the item is placed in the overflow menu. + * + *

Note: Setting an action provider overrides the action view + * set via {@link #setActionView(int)} or {@link #setActionView(View)}. + *

+ * + * @param actionProvider The action provider. + * @return This Item so additional setters can be called. + * @see android.support.v4.view.ActionProvider + */ + public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider); + + /** + * Gets the {@link ActionProvider}. + * + * @return The action provider. + * @see ActionProvider + * @see #setSupportActionProvider(ActionProvider) + */ + public ActionProvider getSupportActionProvider(); + + /** + * Expand the action view associated with this menu item. The menu item must have an action view + * set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setSupportOnActionExpandListener(android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionExpand(MenuItem)} method + * invoked. The listener may return false from this method to prevent expanding the action view. + * + * @return true if the action view was expanded, false otherwise. + */ + public boolean expandActionView(); + + /** + * Collapse the action view associated with this menu item. The menu item must have an action + * view set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setSupportOnActionExpandListener(android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} method + * invoked. The listener may return false from this method to prevent collapsing the action + * view. + * + * @return true if the action view was collapsed, false otherwise. + */ + public boolean collapseActionView(); + + /** + * Returns true if this menu item's action view has been expanded. + * + * @return true if the item's action view is expanded, false otherwise. + * @see #expandActionView() + * @see #collapseActionView() + * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @see android.support.v4.view.MenuItemCompat.OnActionExpandListener + */ + public boolean isActionViewExpanded(); + + /** + * Set an {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener} on this menu item to be notified when the associated + * action view is expanded or collapsed. The menu item must be configured to expand or collapse + * its action view using the flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * + * @param listener Listener that will respond to expand/collapse events + * @return This menu item instance for call chaining + */ + public SupportMenuItem setSupportOnActionExpandListener(MenuItemCompat.OnActionExpandListener listener); +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java new file mode 100644 index 00000000000..4f9bd3939c8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package android.support.v4.internal.view; + +/** + * Subclass of {@link SupportMenu} for sub menus. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.SubMenu + * @hide + */ +public interface SupportSubMenu extends SupportMenu, android.view.SubMenu { +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java new file mode 100644 index 00000000000..39e3a2411f2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java @@ -0,0 +1,359 @@ +/* + * 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. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * A simple set of metadata for a media item suitable for display. This can be + * created using the Builder or retrieved from existing metadata using + * {@link MediaMetadataCompat#getDescription()}. + */ +public final class MediaDescriptionCompat implements Parcelable { + /** + * A unique persistent id for the content or null. + */ + private final String mMediaId; + /** + * A primary title suitable for display or null. + */ + private final CharSequence mTitle; + /** + * A subtitle suitable for display or null. + */ + private final CharSequence mSubtitle; + /** + * A description suitable for display or null. + */ + private final CharSequence mDescription; + /** + * A bitmap icon suitable for display or null. + */ + private final Bitmap mIcon; + /** + * A Uri for an icon suitable for display or null. + */ + private final Uri mIconUri; + /** + * Extras for opaque use by apps/system. + */ + private final Bundle mExtras; + + /** + * A cached copy of the equivalent framework object. + */ + private Object mDescriptionObj; + + private MediaDescriptionCompat(String mediaId, CharSequence title, CharSequence subtitle, + CharSequence description, Bitmap icon, Uri iconUri, Bundle extras) { + mMediaId = mediaId; + mTitle = title; + mSubtitle = subtitle; + mDescription = description; + mIcon = icon; + mIconUri = iconUri; + mExtras = extras; + } + + private MediaDescriptionCompat(Parcel in) { + mMediaId = in.readString(); + mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readParcelable(null); + mIconUri = in.readParcelable(null); + mExtras = in.readBundle(); + } + + /** + * Returns the media id or null. See + * {@link MediaMetadataCompat#METADATA_KEY_MEDIA_ID}. + */ + public String getMediaId() { + return mMediaId; + } + + /** + * Returns a title suitable for display or null. + * + * @return A title or null. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns a subtitle suitable for display or null. + * + * @return A subtitle or null. + */ + public CharSequence getSubtitle() { + return mSubtitle; + } + + /** + * Returns a description suitable for display or null. + * + * @return A description or null. + */ + public CharSequence getDescription() { + return mDescription; + } + + /** + * Returns a bitmap icon suitable for display or null. + * + * @return An icon or null. + */ + public Bitmap getIconBitmap() { + return mIcon; + } + + /** + * Returns a Uri for an icon suitable for display or null. + * + * @return An icon uri or null. + */ + public Uri getIconUri() { + return mIconUri; + } + + /** + * Returns any extras that were added to the description. + * + * @return A bundle of extras or null. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (Build.VERSION.SDK_INT < 21) { + dest.writeString(mMediaId); + TextUtils.writeToParcel(mTitle, dest, flags); + TextUtils.writeToParcel(mSubtitle, dest, flags); + TextUtils.writeToParcel(mDescription, dest, flags); + dest.writeParcelable(mIcon, flags); + dest.writeParcelable(mIconUri, flags); + dest.writeBundle(mExtras); + } else { + MediaDescriptionCompatApi21.writeToParcel(getMediaDescription(), dest, flags); + } + } + + @Override + public String toString() { + return mTitle + ", " + mSubtitle + ", " + mDescription; + } + + /** + * Gets the underlying framework {@link android.media.MediaDescription} + * object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @return An equivalent {@link android.media.MediaDescription} object, or + * null if none. + */ + public Object getMediaDescription() { + if (mDescriptionObj != null || Build.VERSION.SDK_INT < 21) { + return mDescriptionObj; + } + Object bob = MediaDescriptionCompatApi21.Builder.newInstance(); + MediaDescriptionCompatApi21.Builder.setMediaId(bob, mMediaId); + MediaDescriptionCompatApi21.Builder.setTitle(bob, mTitle); + MediaDescriptionCompatApi21.Builder.setSubtitle(bob, mSubtitle); + MediaDescriptionCompatApi21.Builder.setDescription(bob, mDescription); + MediaDescriptionCompatApi21.Builder.setIconBitmap(bob, mIcon); + MediaDescriptionCompatApi21.Builder.setIconUri(bob, mIconUri); + MediaDescriptionCompatApi21.Builder.setExtras(bob, mExtras); + mDescriptionObj = MediaDescriptionCompatApi21.Builder.build(bob); + + return mDescriptionObj; + } + + /** + * Creates an instance from a framework + * {@link android.media.MediaDescription} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @param descriptionObj A {@link android.media.MediaDescription} object, or + * null if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if + * none. + */ + public static MediaDescriptionCompat fromMediaDescription(Object descriptionObj) { + if (descriptionObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + Builder bob = new Builder(); + bob.setMediaId(MediaDescriptionCompatApi21.getMediaId(descriptionObj)); + bob.setTitle(MediaDescriptionCompatApi21.getTitle(descriptionObj)); + bob.setSubtitle(MediaDescriptionCompatApi21.getSubtitle(descriptionObj)); + bob.setDescription(MediaDescriptionCompatApi21.getDescription(descriptionObj)); + bob.setIconBitmap(MediaDescriptionCompatApi21.getIconBitmap(descriptionObj)); + bob.setIconUri(MediaDescriptionCompatApi21.getIconUri(descriptionObj)); + bob.setExtras(MediaDescriptionCompatApi21.getExtras(descriptionObj)); + MediaDescriptionCompat descriptionCompat = bob.build(); + descriptionCompat.mDescriptionObj = descriptionObj; + + return descriptionCompat; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaDescriptionCompat createFromParcel(Parcel in) { + if (Build.VERSION.SDK_INT < 21) { + return new MediaDescriptionCompat(in); + } else { + return fromMediaDescription(MediaDescriptionCompatApi21.fromParcel(in)); + } + } + + @Override + public MediaDescriptionCompat[] newArray(int size) { + return new MediaDescriptionCompat[size]; + } + }; + + /** + * Builder for {@link MediaDescriptionCompat} objects. + */ + public static final class Builder { + private String mMediaId; + private CharSequence mTitle; + private CharSequence mSubtitle; + private CharSequence mDescription; + private Bitmap mIcon; + private Uri mIconUri; + private Bundle mExtras; + + /** + * Creates an initially empty builder. + */ + public Builder() { + } + + /** + * Sets the media id. + * + * @param mediaId The unique id for the item or null. + * @return this + */ + public Builder setMediaId(String mediaId) { + mMediaId = mediaId; + return this; + } + + /** + * Sets the title. + * + * @param title A title suitable for display to the user or null. + * @return this + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets the subtitle. + * + * @param subtitle A subtitle suitable for display to the user or null. + * @return this + */ + public Builder setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + return this; + } + + /** + * Sets the description. + * + * @param description A description suitable for display to the user or + * null. + * @return this + */ + public Builder setDescription(CharSequence description) { + mDescription = description; + return this; + } + + /** + * Sets the icon. + * + * @param icon A {@link Bitmap} icon suitable for display to the user or + * null. + * @return this + */ + public Builder setIconBitmap(Bitmap icon) { + mIcon = icon; + return this; + } + + /** + * Sets the icon uri. + * + * @param iconUri A {@link Uri} for an icon suitable for display to the + * user or null. + * @return this + */ + public Builder setIconUri(Uri iconUri) { + mIconUri = iconUri; + return this; + } + + /** + * Sets a bundle of extras. + * + * @param extras The extras to include with this description or null. + * @return this + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates a {@link MediaDescriptionCompat} instance with the specified + * fields. + * + * @return A MediaDescriptionCompat instance. + */ + public MediaDescriptionCompat build() { + return new MediaDescriptionCompat(mMediaId, mTitle, mSubtitle, mDescription, mIcon, + mIconUri, mExtras); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java new file mode 100644 index 00000000000..991515acccb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java @@ -0,0 +1,100 @@ +/* + * 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. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.media.MediaDescription; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; + +public class MediaDescriptionCompatApi21 { + + public static String getMediaId(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getMediaId(); + } + + public static CharSequence getTitle(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getTitle(); + } + + public static CharSequence getSubtitle(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getSubtitle(); + } + + public static CharSequence getDescription(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getDescription(); + } + + public static Bitmap getIconBitmap(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getIconBitmap(); + } + + public static Uri getIconUri(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getIconUri(); + } + + public static Bundle getExtras(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getExtras(); + } + + public static void writeToParcel(Object descriptionObj, Parcel dest, int flags) { + ((MediaDescription) descriptionObj).writeToParcel(dest, flags); + } + + public static Object fromParcel(Parcel in) { + return MediaDescription.CREATOR.createFromParcel(in); + } + + public static class Builder { + public static Object newInstance() { + return new MediaDescription.Builder(); + } + + + public static void setMediaId(Object builderObj, String mediaId) { + ((MediaDescription.Builder)builderObj).setMediaId(mediaId); + } + + public static void setTitle(Object builderObj, CharSequence title) { + ((MediaDescription.Builder)builderObj).setTitle(title); + } + + public static void setSubtitle(Object builderObj, CharSequence subtitle) { + ((MediaDescription.Builder)builderObj).setSubtitle(subtitle); + } + + public static void setDescription(Object builderObj, CharSequence description) { + ((MediaDescription.Builder)builderObj).setDescription(description); + } + + public static void setIconBitmap(Object builderObj, Bitmap iconBitmap) { + ((MediaDescription.Builder)builderObj).setIconBitmap(iconBitmap); + } + + public static void setIconUri(Object builderObj, Uri iconUri) { + ((MediaDescription.Builder)builderObj).setIconUri(iconUri); + } + + public static void setExtras(Object builderObj, Bundle extras) { + ((MediaDescription.Builder)builderObj).setExtras(extras); + } + + public static Object build(Object builderObj) { + return ((MediaDescription.Builder) builderObj).build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl new file mode 100644 index 00000000000..6d36b97eca3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media; + +parcelable MediaMetadataCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java new file mode 100644 index 00000000000..38074807053 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java @@ -0,0 +1,746 @@ +/* + * 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. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.ArrayMap; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Set; + +/** + * Contains metadata about an item, such as the title, artist, etc. + */ +public final class MediaMetadataCompat implements Parcelable { + private static final String TAG = "MediaMetadata"; + + /** + * The title of the media. + */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** + * The artist of the media. + */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A negative duration indicates that the + * duration is unknown (or infinite). + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** + * The album title for the media. + */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** + * The author of the media. + */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** + * The writer of the media. + */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** + * The composer of the media. + */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** + * The compilation status of the media. + */ + public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + + /** + * The date the media was created or published as TODO determine format. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** + * The year the media was created or published as a long. + */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** + * The genre of the media. + */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** + * The track number for the media. + */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** + * The number of tracks in the media's original source. + */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** + * The disc number for the media's original source. + */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** + * The artist for the album of the media's original source. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** + * The artwork for the media as a Uri style String. + */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a + * {@link Bitmap}. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** + * The artwork for the album of the media's original source as a Uri style + * String. + */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + /** + * A title that is suitable for display to the user. This will generally be + * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats. + * When displaying media described by this metadata this should be preferred + * if present. + */ + public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE"; + + /** + * A subtitle that is suitable for display to the user. When displaying a + * second line for media described by this metadata this should be preferred + * to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_SUBTITLE + = "android.media.metadata.DISPLAY_SUBTITLE"; + + /** + * A description that is suitable for display to the user. When displaying + * more information for media described by this metadata this should be + * preferred to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_DESCRIPTION + = "android.media.metadata.DISPLAY_DESCRIPTION"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying an icon for media described by this metadata this should be + * preferred to other fields if present. This must be a {@link Bitmap}. + */ + public static final String METADATA_KEY_DISPLAY_ICON + = "android.media.metadata.DISPLAY_ICON"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying more information for media described by this metadata the + * display description should be preferred to other fields when present. + * This must be a Uri style String. + */ + public static final String METADATA_KEY_DISPLAY_ICON_URI + = "android.media.metadata.DISPLAY_ICON_URI"; + + /** + * A String key for identifying the content. This value is specific to the + * service providing the content. If used, this should be a persistent + * unique key for the underlying content. + */ + public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID"; + + private static final int METADATA_TYPE_LONG = 0; + private static final int METADATA_TYPE_TEXT = 1; + private static final int METADATA_TYPE_BITMAP = 2; + private static final int METADATA_TYPE_RATING = 3; + private static final ArrayMap METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap(); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT); + } + + private static final String[] PREFERRED_DESCRIPTION_ORDER = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_WRITER, + METADATA_KEY_AUTHOR, + METADATA_KEY_COMPOSER + }; + + private static final String[] PREFERRED_BITMAP_ORDER = { + METADATA_KEY_DISPLAY_ICON, + METADATA_KEY_ART, + METADATA_KEY_ALBUM_ART + }; + + private static final String[] PREFERRED_URI_ORDER = { + METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI + }; + + private final Bundle mBundle; + private Object mMetadataObj; + private MediaDescriptionCompat mDescription; + + private MediaMetadataCompat(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private MediaMetadataCompat(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns true if the given key is contained in the metadata + * + * @param key a String key + * @return true if the key exists in this metadata, false otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a CharSequence value, or null + */ + public CharSequence getText(String key) { + return mBundle.getCharSequence(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + CharSequence text = mBundle.getCharSequence(key); + if (text != null) { + return text.toString(); + } + return null; + } + + /** + * Returns the value associated with the given key, or 0L if no long exists + * for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(String key) { + return mBundle.getLong(key, 0); + } + + /** + * Return a {@link RatingCompat} for the given key or null if no rating exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link RatingCompat} or null + */ + public RatingCompat getRating(String key) { + RatingCompat rating = null; + try { + rating = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Rating.", e); + } + return rating; + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + /** + * Returns a simple description of this metadata for display purposes. + * + * @return A simple description of this metadata. + */ + public MediaDescriptionCompat getDescription() { + if (mDescription != null) { + return mDescription; + } + + String mediaId = getString(METADATA_KEY_MEDIA_ID); + + CharSequence[] text = new CharSequence[3]; + Bitmap icon = null; + Uri iconUri = null; + + // First handle the case where display data is set already + CharSequence displayText = getText(METADATA_KEY_DISPLAY_TITLE); + if (!TextUtils.isEmpty(displayText)) { + // If they have a display title use only display data, otherwise use + // our best bets + text[0] = displayText; + text[1] = getText(METADATA_KEY_DISPLAY_SUBTITLE); + text[2] = getText(METADATA_KEY_DISPLAY_DESCRIPTION); + } else { + // Use whatever fields we can + int textIndex = 0; + int keyIndex = 0; + while (textIndex < text.length && keyIndex < PREFERRED_DESCRIPTION_ORDER.length) { + CharSequence next = getText(PREFERRED_DESCRIPTION_ORDER[keyIndex++]); + if (!TextUtils.isEmpty(next)) { + // Fill in the next empty bit of text + text[textIndex++] = next; + } + } + } + + // Get the best art bitmap we can find + for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) { + Bitmap next = getBitmap(PREFERRED_BITMAP_ORDER[i]); + if (next != null) { + icon = next; + break; + } + } + + // Get the best Uri we can find + for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) { + String next = getString(PREFERRED_URI_ORDER[i]); + if (!TextUtils.isEmpty(next)) { + iconUri = Uri.parse(next); + break; + } + } + + MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder(); + bob.setMediaId(mediaId); + bob.setTitle(text[0]); + bob.setSubtitle(text[1]); + bob.setDescription(text[2]); + bob.setIconBitmap(icon); + bob.setIconUri(iconUri); + mDescription = bob.build(); + + return mDescription; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Get the number of fields in this metadata. + * + * @return The number of fields in the metadata. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this metadata. + * + * @return a Set of String keys + */ + public Set keySet() { + return mBundle.keySet(); + } + + /** + * Gets the bundle backing the metadata object. This is available to support + * backwards compatibility. Apps should not modify the bundle directly. + * + * @return The Bundle backing this metadata. + */ + public Bundle getBundle() { + return mBundle; + } + + /** + * Creates an instance from a framework {@link android.media.MediaMetadata} + * object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @param metadataObj A {@link android.media.MediaMetadata} object, or null + * if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if + * none. + */ + public static MediaMetadataCompat fromMediaMetadata(Object metadataObj) { + if (metadataObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + Builder builder = new Builder(); + for (String key : MediaMetadataCompatApi21.keySet(metadataObj)) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + builder.putBitmap(key, + MediaMetadataCompatApi21.getBitmap(metadataObj, key)); + break; + case METADATA_TYPE_LONG: + builder.putLong(key, + MediaMetadataCompatApi21.getLong(metadataObj, key)); + break; + case METADATA_TYPE_RATING: + builder.putRating(key, RatingCompat.fromRating( + MediaMetadataCompatApi21.getRating(metadataObj, key))); + break; + case METADATA_TYPE_TEXT: + builder.putText(key, + MediaMetadataCompatApi21.getText(metadataObj, key)); + break; + } + } + } + MediaMetadataCompat metadata = builder.build(); + metadata.mMetadataObj = metadataObj; + return metadata; + } + + /** + * Gets the underlying framework {@link android.media.MediaMetadata} object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @return An equivalent {@link android.media.MediaMetadata} object, or null + * if none. + */ + public Object getMediaMetadata() { + if (mMetadataObj != null || Build.VERSION.SDK_INT < 21) { + return mMetadataObj; + } + + Object builderObj = MediaMetadataCompatApi21.Builder.newInstance(); + for (String key : keySet()) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + MediaMetadataCompatApi21.Builder.putBitmap(builderObj, key, + getBitmap(key)); + break; + case METADATA_TYPE_LONG: + MediaMetadataCompatApi21.Builder.putLong(builderObj, key, + getLong(key)); + break; + case METADATA_TYPE_RATING: + MediaMetadataCompatApi21.Builder.putRating(builderObj, key, + getRating(key).getRating()); + break; + case METADATA_TYPE_TEXT: + MediaMetadataCompatApi21.Builder.putText(builderObj, key, + getText(key)); + break; + } + } + } + mMetadataObj = MediaMetadataCompatApi21.Builder.build(builderObj); + return mMetadataObj; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaMetadataCompat createFromParcel(Parcel in) { + return new MediaMetadataCompat(in); + } + + @Override + public MediaMetadataCompat[] newArray(int size) { + return new MediaMetadataCompat[size]; + } + }; + + /** + * Use to build MediaMetadata objects. The system defined metadata keys must + * use the appropriate data type. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link MediaMetadataCompat} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadataCompat} instance to set the + * initial values. All fields in the source metadata will be included in + * the new metadata. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(MediaMetadataCompat source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Put a CharSequence value into the metadata. Custom keys may be used, + * but if the METADATA_KEYs defined in this class are used they may only + * be one of the following: + *
    + *
  • {@link #METADATA_KEY_TITLE}
  • + *
  • {@link #METADATA_KEY_ARTIST}
  • + *
  • {@link #METADATA_KEY_ALBUM}
  • + *
  • {@link #METADATA_KEY_AUTHOR}
  • + *
  • {@link #METADATA_KEY_WRITER}
  • + *
  • {@link #METADATA_KEY_COMPOSER}
  • + *
  • {@link #METADATA_KEY_DATE}
  • + *
  • {@link #METADATA_KEY_GENRE}
  • + *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • + *
  • {@link #METADATA_KEY_ART_URI}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • + *
  • {@link #METADATA_KEY_DISPLAY_TITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI}
  • + *
+ * + * @param key The key for referencing this value + * @param value The CharSequence value to store + * @return The Builder to allow chaining + */ + public Builder putText(String key, CharSequence value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a CharSequence"); + } + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + *
    + *
  • {@link #METADATA_KEY_TITLE}
  • + *
  • {@link #METADATA_KEY_ARTIST}
  • + *
  • {@link #METADATA_KEY_ALBUM}
  • + *
  • {@link #METADATA_KEY_AUTHOR}
  • + *
  • {@link #METADATA_KEY_WRITER}
  • + *
  • {@link #METADATA_KEY_COMPOSER}
  • + *
  • {@link #METADATA_KEY_DATE}
  • + *
  • {@link #METADATA_KEY_GENRE}
  • + *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • + *
  • {@link #METADATA_KEY_ART_URI}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • + *
  • {@link #METADATA_KEY_DISPLAY_TITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putString(String key, String value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + *
    + *
  • {@link #METADATA_KEY_DURATION}
  • + *
  • {@link #METADATA_KEY_TRACK_NUMBER}
  • + *
  • {@link #METADATA_KEY_NUM_TRACKS}
  • + *
  • {@link #METADATA_KEY_DISC_NUMBER}
  • + *
  • {@link #METADATA_KEY_YEAR}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putLong(String key, long value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a long"); + } + } + mBundle.putLong(key, value); + return this; + } + + /** + * Put a {@link RatingCompat} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + *
    + *
  • {@link #METADATA_KEY_RATING}
  • + *
  • {@link #METADATA_KEY_USER_RATING}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putRating(String key, RatingCompat value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Rating"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + *
    + *
  • {@link #METADATA_KEY_ART}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON}
  • + *
+ * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public Builder putBitmap(String key, Bitmap value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link MediaMetadataCompat} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadataCompat build() { + return new MediaMetadataCompat(mBundle); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java new file mode 100644 index 00000000000..eddcf769445 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.media.Rating; + +import java.util.Set; + +class MediaMetadataCompatApi21 { + public static Set keySet(Object metadataObj) { + return ((MediaMetadata)metadataObj).keySet(); + } + + public static Bitmap getBitmap(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getBitmap(key); + } + + public static long getLong(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getLong(key); + } + + public static Object getRating(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getRating(key); + } + + public static CharSequence getText(Object metadataObj, String key) { + return ((MediaMetadata) metadataObj).getText(key); + } + + public static class Builder { + public static Object newInstance() { + return new MediaMetadata.Builder(); + } + + public static void putBitmap(Object builderObj, String key, Bitmap value) { + ((MediaMetadata.Builder)builderObj).putBitmap(key, value); + } + + public static void putLong(Object builderObj, String key, long value) { + ((MediaMetadata.Builder)builderObj).putLong(key, value); + } + + public static void putRating(Object builderObj, String key, Object ratingObj) { + ((MediaMetadata.Builder)builderObj).putRating(key, (Rating)ratingObj); + } + + public static void putText(Object builderObj, String key, CharSequence value) { + ((MediaMetadata.Builder) builderObj).putText(key, value); + } + + public static void putString(Object builderObj, String key, String value) { + ((MediaMetadata.Builder) builderObj).putString(key, value); + } + + public static Object build(Object builderObj) { + return ((MediaMetadata.Builder)builderObj).build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl new file mode 100644 index 00000000000..223fd5c96f7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media; + +parcelable RatingCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java new file mode 100644 index 00000000000..c7da6ec4a03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java @@ -0,0 +1,370 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * A class to encapsulate rating information used as content metadata. + * A rating is defined by its rating style (see {@link #RATING_HEART}, + * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may + * be defined as "unrated"), both of which are defined when the rating instance is constructed + * through one of the factory methods. + */ +public final class RatingCompat implements Parcelable { + private final static String TAG = "Rating"; + + /** + * Indicates a rating style is not supported. A Rating will never have this + * type, but can be used by other classes to indicate they do not support + * Rating. + */ + public final static int RATING_NONE = 0; + + /** + * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to + * indicate the content referred to is a favorite (or not). + */ + public final static int RATING_HEART = 1; + + /** + * A rating style for "thumb up" vs "thumb down". + */ + public final static int RATING_THUMB_UP_DOWN = 2; + + /** + * A rating style with 0 to 3 stars. + */ + public final static int RATING_3_STARS = 3; + + /** + * A rating style with 0 to 4 stars. + */ + public final static int RATING_4_STARS = 4; + + /** + * A rating style with 0 to 5 stars. + */ + public final static int RATING_5_STARS = 5; + + /** + * A rating style expressed as a percentage. + */ + public final static int RATING_PERCENTAGE = 6; + + private final static float RATING_NOT_RATED = -1.0f; + + private final int mRatingStyle; + private final float mRatingValue; + + private Object mRatingObj; // framework Rating object + + private RatingCompat(int ratingStyle, float rating) { + mRatingStyle = ratingStyle; + mRatingValue = rating; + } + + @Override + public String toString() { + return "Rating:style=" + mRatingStyle + " rating=" + + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue)); + } + + @Override + public int describeContents() { + return mRatingStyle; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRatingStyle); + dest.writeFloat(mRatingValue); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + /** + * Rebuilds a Rating previously stored with writeToParcel(). + * @param p Parcel object to read the Rating from + * @return a new Rating created from the data in the parcel + */ + @Override + public RatingCompat createFromParcel(Parcel p) { + return new RatingCompat(p.readInt(), p.readFloat()); + } + + @Override + public RatingCompat[] newArray(int size) { + return new RatingCompat[size]; + } + }; + + /** + * Return a Rating instance with no rating. + * Create and return a new Rating instance with no rating known for the given + * rating style. + * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + * @return null if an invalid rating style is passed, a new Rating instance otherwise. + */ + public static RatingCompat newUnratedRating(int ratingStyle) { + switch(ratingStyle) { + case RATING_HEART: + case RATING_THUMB_UP_DOWN: + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + case RATING_PERCENTAGE: + return new RatingCompat(ratingStyle, RATING_NOT_RATED); + default: + return null; + } + } + + /** + * Return a Rating instance with a heart-based rating. + * Create and return a new Rating instance with a rating style of {@link #RATING_HEART}, + * and a heart-based rating. + * @param hasHeart true for a "heart selected" rating, false for "heart unselected". + * @return a new Rating instance. + */ + public static RatingCompat newHeartRating(boolean hasHeart) { + return new RatingCompat(RATING_HEART, hasHeart ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a thumb-based rating. + * Create and return a new Rating instance with a {@link #RATING_THUMB_UP_DOWN} + * rating style, and a "thumb up" or "thumb down" rating. + * @param thumbIsUp true for a "thumb up" rating, false for "thumb down". + * @return a new Rating instance. + */ + public static RatingCompat newThumbRating(boolean thumbIsUp) { + return new RatingCompat(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a star-based rating. + * Create and return a new Rating instance with one of the star-base rating styles + * and the given integer or fractional number of stars. Non integer values can for instance + * be used to represent an average rating value, which might not be an integer number of stars. + * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS}. + * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to + * the rating style. + * @return null if the rating style is invalid, or the rating is out of range, + * a new Rating instance otherwise. + */ + public static RatingCompat newStarRating(int starRatingStyle, float starRating) { + float maxRating = -1.0f; + switch(starRatingStyle) { + case RATING_3_STARS: + maxRating = 3.0f; + break; + case RATING_4_STARS: + maxRating = 4.0f; + break; + case RATING_5_STARS: + maxRating = 5.0f; + break; + default: + Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating"); + return null; + } + if ((starRating < 0.0f) || (starRating > maxRating)) { + Log.e(TAG, "Trying to set out of range star-based rating"); + return null; + } + return new RatingCompat(starRatingStyle, starRating); + } + + /** + * Return a Rating instance with a percentage-based rating. + * Create and return a new Rating instance with a {@link #RATING_PERCENTAGE} + * rating style, and a rating of the given percentage. + * @param percent the value of the rating + * @return null if the rating is out of range, a new Rating instance otherwise. + */ + public static RatingCompat newPercentageRating(float percent) { + if ((percent < 0.0f) || (percent > 100.0f)) { + Log.e(TAG, "Invalid percentage-based rating value"); + return null; + } else { + return new RatingCompat(RATING_PERCENTAGE, percent); + } + } + + /** + * Return whether there is a rating value available. + * @return true if the instance was not created with {@link #newUnratedRating(int)}. + */ + public boolean isRated() { + return mRatingValue >= 0.0f; + } + + /** + * Return the rating style. + * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + */ + public int getRatingStyle() { + return mRatingStyle; + } + + /** + * Return whether the rating is "heart selected". + * @return true if the rating is "heart selected", false if the rating is "heart unselected", + * if the rating style is not {@link #RATING_HEART} or if it is unrated. + */ + public boolean hasHeart() { + if (mRatingStyle != RATING_HEART) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return whether the rating is "thumb up". + * @return true if the rating is "thumb up", false if the rating is "thumb down", + * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated. + */ + public boolean isThumbUp() { + if (mRatingStyle != RATING_THUMB_UP_DOWN) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return the star-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not star-based, or if it is unrated. + */ + public float getStarRating() { + switch (mRatingStyle) { + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + if (isRated()) { + return mRatingValue; + } + default: + return -1.0f; + } + } + + /** + * Return the percentage-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not percentage-based, or if it is unrated. + */ + public float getPercentRating() { + if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) { + return -1.0f; + } else { + return mRatingValue; + } + } + + /** + * Creates an instance from a framework {@link android.media.Rating} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @param ratingObj A {@link android.media.Rating} object, or null if none. + * @return An equivalent {@link RatingCompat} object, or null if none. + */ + public static RatingCompat fromRating(Object ratingObj) { + if (ratingObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + final int ratingStyle = RatingCompatApi21.getRatingStyle(ratingObj); + final RatingCompat rating; + if (RatingCompatApi21.isRated(ratingObj)) { + switch (ratingStyle) { + case RATING_HEART: + rating = newHeartRating(RatingCompatApi21.hasHeart(ratingObj)); + break; + case RATING_THUMB_UP_DOWN: + rating = newThumbRating(RatingCompatApi21.isThumbUp(ratingObj)); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + rating = newStarRating(ratingStyle, + RatingCompatApi21.getStarRating(ratingObj)); + break; + case RATING_PERCENTAGE: + rating = newPercentageRating(RatingCompatApi21.getPercentRating(ratingObj)); + break; + default: + return null; + } + } else { + rating = newUnratedRating(ratingStyle); + } + rating.mRatingObj = ratingObj; + return rating; + } + + /** + * Gets the underlying framework {@link android.media.Rating} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return An equivalent {@link android.media.Rating} object, or null if none. + */ + public Object getRating() { + if (mRatingObj != null || Build.VERSION.SDK_INT < 21) { + return mRatingObj; + } + + if (isRated()) { + switch (mRatingStyle) { + case RATING_HEART: + mRatingObj = RatingCompatApi21.newHeartRating(hasHeart()); + break; + case RATING_THUMB_UP_DOWN: + mRatingObj = RatingCompatApi21.newThumbRating(isThumbUp()); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + mRatingObj = RatingCompatApi21.newStarRating(mRatingStyle, getStarRating()); + break; + case RATING_PERCENTAGE: + mRatingObj = RatingCompatApi21.newPercentageRating(getPercentRating()); + default: + return null; + } + } else { + mRatingObj = RatingCompatApi21.newUnratedRating(mRatingStyle); + } + return mRatingObj; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java new file mode 100644 index 00000000000..6c8b9b2b4f4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.media.Rating; + +class RatingCompatApi21 { + public static Object newUnratedRating(int ratingStyle) { + return Rating.newUnratedRating(ratingStyle); + } + + public static Object newHeartRating(boolean hasHeart) { + return Rating.newHeartRating(hasHeart); + } + + public static Object newThumbRating(boolean thumbIsUp) { + return Rating.newThumbRating(thumbIsUp); + } + + public static Object newStarRating(int starRatingStyle, float starRating) { + return Rating.newStarRating(starRatingStyle, starRating); + } + + public static Object newPercentageRating(float percent) { + return Rating.newPercentageRating(percent); + } + + public static boolean isRated(Object ratingObj) { + return ((Rating)ratingObj).isRated(); + } + + public static int getRatingStyle(Object ratingObj) { + return ((Rating)ratingObj).getRatingStyle(); + } + + public static boolean hasHeart(Object ratingObj) { + return ((Rating)ratingObj).hasHeart(); + } + + public static boolean isThumbUp(Object ratingObj) { + return ((Rating)ratingObj).isThumbUp(); + } + + public static float getStarRating(Object ratingObj) { + return ((Rating)ratingObj).getStarRating(); + } + + public static float getPercentRating(Object ratingObj) { + return ((Rating)ratingObj).getPercentRating(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java new file mode 100644 index 00000000000..b92a4a10f81 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package android.support.v4.media; + +/** + * 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. + */ +public abstract class TransportController { + /** + * Start listening to changes in playback state. + */ + public abstract void registerStateListener(TransportStateListener listener); + + /** + * Stop listening to changes in playback state. + */ + public abstract void unregisterStateListener(TransportStateListener listener); + + /** + * Request that the player start its playback at its current position. + */ + public abstract void startPlaying(); + + /** + * Request that the player pause its playback and stay at its current position. + */ + public abstract void pausePlaying(); + + /** + * Request that the player stop its playback; it may clear its state in whatever + * way is appropriate. + */ + public abstract void stopPlaying(); + + /** + * Retrieve the total duration of the media stream, in milliseconds. + */ + public abstract long getDuration(); + + /** + * Retrieve the current playback location in the media stream, in milliseconds. + */ + public abstract long getCurrentPosition(); + + /** + * Move to a new location in the media stream. + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Return whether the player is currently playing its stream. + */ + 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. + */ + public abstract int getBufferPercentage(); + + /** + * Retrieve the flags for the media transport control buttons that this transport supports. + * Result is a combination of the following flags: + * {@link TransportMediator#FLAG_KEY_MEDIA_PREVIOUS}, + * {@link TransportMediator#FLAG_KEY_MEDIA_REWIND}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}, + * {@link TransportMediator#FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link TransportMediator#FLAG_KEY_MEDIA_NEXT} + */ + public abstract int getTransportControlFlags(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java new file mode 100644 index 00000000000..16bf94b558e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java @@ -0,0 +1,343 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.app.Activity; +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; +import android.support.v4.view.KeyEventCompat; +import android.view.KeyEvent; +import android.view.View; + +import java.util.ArrayList; + +/** + * Helper for implementing a media transport control (with play, pause, skip, and + * other media actions). Takes care of both key events and advanced features + * like {@link android.media.RemoteControlClient}. This class is intended to + * serve as an intermediary between transport controls (whether they be on-screen + * controls, hardware buttons, remote controls) and the actual player. The player + * is represented by a single {@link TransportPerformer} that must be supplied to + * this class. On-screen controls that want to control and show the state of the + * player should do this through calls to the {@link TransportController} interface. + * + *

Here is a simple but fairly complete sample of a video player that is built + * around this class. Note that the MediaController class used here is not the one + * included in the standard Android framework, but a custom implementation. Real + * applications often implement their own transport controls, or you can copy the + * implementation here out of Support4Demos.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java + * complete} + */ +public class TransportMediator extends TransportController { + final Context mContext; + final TransportPerformer mCallbacks; + final AudioManager mAudioManager; + final View mView; + final Object mDispatcherState; + final TransportMediatorJellybeanMR2 mController; + final ArrayList mListeners + = new ArrayList(); + final TransportMediatorCallback mTransportKeyCallback + = new TransportMediatorCallback() { + @Override + public void handleKey(KeyEvent key) { + key.dispatch(mKeyEventCallback); + } + @Override + public void handleAudioFocusChange(int focusChange) { + mCallbacks.onAudioFocusChange(focusChange); + } + + @Override + public long getPlaybackPosition() { + return mCallbacks.onGetCurrentPosition(); + } + + @Override + public void playbackPositionUpdate(long newPositionMs) { + mCallbacks.onSeekTo(newPositionMs); + } + }; + + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY} */ + public static final int KEYCODE_MEDIA_PLAY = 126; + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE} */ + public static final int KEYCODE_MEDIA_PAUSE = 127; + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD} */ + public static final int KEYCODE_MEDIA_RECORD = 130; + + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS + * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS */ + public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND + * RemoteControlClient.FLAG_KEY_MEDIA_REWIND */ + 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 */ + 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 */ + 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 */ + 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 */ + 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 */ + 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 */ + public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7; + + static boolean isMediaKey(int keyCode) { + switch (keyCode) { + case KEYCODE_MEDIA_PLAY: + case KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + return true; + } + } + return false; + } + + final KeyEvent.Callback mKeyEventCallback = new KeyEvent.Callback() { + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return isMediaKey(keyCode) ? mCallbacks.onMediaButtonDown(keyCode, event) : false; + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return isMediaKey(keyCode) ? mCallbacks.onMediaButtonUp(keyCode, event) : false; + } + + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + }; + + public TransportMediator(Activity activity, TransportPerformer callbacks) { + this(activity, null, callbacks); + } + + public TransportMediator(View view, TransportPerformer callbacks) { + this(null, view, callbacks); + } + + private TransportMediator(Activity activity, View view, TransportPerformer callbacks) { + mContext = activity != null ? activity : view.getContext(); + mCallbacks = callbacks; + mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); + mView = activity != null ? activity.getWindow().getDecorView() : view; + mDispatcherState = KeyEventCompat.getKeyDispatcherState(mView); + if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + mController = new TransportMediatorJellybeanMR2(mContext, mAudioManager, + mView, mTransportKeyCallback); + } else { + mController = null; + } + } + + /** + * Return the {@link android.media.RemoteControlClient} associated with this transport. + * This returns a generic Object since the RemoteControlClient is not availble before + * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}. Further, this class + * will not use RemoteControlClient in its implementation until + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}. You should always check for + * null here and not do anything with the RemoteControlClient if none is given; this + * way you don't need to worry about the current platform API version. + * + *

Note that this class takes possession of the + * {@link android.media.RemoteControlClient.OnGetPlaybackPositionListener} and + * {@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener} callbacks; + * you will interact with these through + * {@link TransportPerformer#onGetCurrentPosition() TransportPerformer.onGetCurrentPosition} and + * {@link TransportPerformer#onSeekTo TransportPerformer.onSeekTo}, respectively.

+ */ + public Object getRemoteControlClient() { + return mController != null ? mController.getRemoteControlClient() : null; + } + + /** + * Must call from {@link Activity#dispatchKeyEvent Activity.dispatchKeyEvent} to give + * the transport an opportunity to intercept media keys. Any such keys will show up + * in {@link TransportPerformer}. + * @param event + */ + public boolean dispatchKeyEvent(KeyEvent event) { + return KeyEventCompat.dispatch(event, mKeyEventCallback, mDispatcherState, this); + } + + public void registerStateListener(TransportStateListener listener) { + mListeners.add(listener); + } + + public void unregisterStateListener(TransportStateListener listener) { + mListeners.remove(listener); + } + + private TransportStateListener[] getListeners() { + if (mListeners.size() <= 0) { + return null; + } + TransportStateListener listeners[] = new TransportStateListener[mListeners.size()]; + mListeners.toArray(listeners); + return listeners; + } + + private void reportPlayingChanged() { + TransportStateListener[] listeners = getListeners(); + if (listeners != null) { + for (TransportStateListener listener : listeners) { + listener.onPlayingChanged(this); + } + } + } + + private void reportTransportControlsChanged() { + TransportStateListener[] listeners = getListeners(); + if (listeners != null) { + for (TransportStateListener listener : listeners) { + listener.onTransportControlsChanged(this); + } + } + } + + private void pushControllerState() { + if (mController != null) { + mController.refreshState(mCallbacks.onIsPlaying(), + mCallbacks.onGetCurrentPosition(), + mCallbacks.onGetTransportControlFlags()); + } + } + + public void refreshState() { + pushControllerState(); + reportPlayingChanged(); + reportTransportControlsChanged(); + } + + /** + * 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. + */ + @Override + public void startPlaying() { + if (mController != null) { + mController.startPlaying(); + } + mCallbacks.onStart(); + pushControllerState(); + reportPlayingChanged(); + } + + /** + * Move the controller into the paused state. This updates the remote control + * client to indicate it is paused, but keeps audio focus. + */ + @Override + public void pausePlaying() { + if (mController != null) { + mController.pausePlaying(); + } + mCallbacks.onPause(); + pushControllerState(); + reportPlayingChanged(); + } + + /** + * 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. + */ + @Override + public void stopPlaying() { + if (mController != null) { + mController.stopPlaying(); + } + mCallbacks.onStop(); + pushControllerState(); + reportPlayingChanged(); + } + + @Override + public long getDuration() { + return mCallbacks.onGetDuration(); + } + + @Override + public long getCurrentPosition() { + return mCallbacks.onGetCurrentPosition(); + } + + @Override + public void seekTo(long pos) { + mCallbacks.onSeekTo(pos); + } + + @Override + public boolean isPlaying() { + return mCallbacks.onIsPlaying(); + } + + @Override + public int getBufferPercentage() { + return mCallbacks.onGetBufferPercentage(); + } + + /** + * Retrieves 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}, + * {@link #FLAG_KEY_MEDIA_PLAY}, + * {@link #FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link #FLAG_KEY_MEDIA_PAUSE}, + * {@link #FLAG_KEY_MEDIA_STOP}, + * {@link #FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link #FLAG_KEY_MEDIA_NEXT} + */ + public int getTransportControlFlags() { + return mCallbacks.onGetTransportControlFlags(); + } + + /** + * 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. + */ + public void destroy() { + mController.destroy(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java new file mode 100644 index 00000000000..7dae7dc1461 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.view.KeyEvent; + +interface TransportMediatorCallback { + public void handleKey(KeyEvent key); + public void handleAudioFocusChange(int focusChange); + public long getPlaybackPosition(); + public void playbackPositionUpdate(long newPositionMs); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java new file mode 100644 index 00000000000..38b6778f23e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java @@ -0,0 +1,207 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewTreeObserver; + +class TransportMediatorJellybeanMR2 + implements RemoteControlClient.OnGetPlaybackPositionListener, + RemoteControlClient.OnPlaybackPositionUpdateListener { + final Context mContext; + final AudioManager mAudioManager; + final View mTargetView; + final TransportMediatorCallback mTransportCallback; + final String mReceiverAction; + final IntentFilter mReceiverFilter; + final Intent mIntent; + final ViewTreeObserver.OnWindowAttachListener mWindowAttachListener = + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + windowAttached(); + } + @Override + public void onWindowDetached() { + windowDetached(); + } + }; + final ViewTreeObserver.OnWindowFocusChangeListener mWindowFocusListener = + new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) gainFocus(); + else loseFocus(); + } + }; + final BroadcastReceiver mMediaButtonReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + try { + KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + mTransportCallback.handleKey(event); + } catch (ClassCastException e) { + Log.w("TransportController", e); + } + } + }; + AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener + = new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) { + mTransportCallback.handleAudioFocusChange(focusChange); + } + }; + + PendingIntent mPendingIntent; + RemoteControlClient mRemoteControl; + boolean mFocused; + int mPlayState = 0; + boolean mAudioFocused; + + public TransportMediatorJellybeanMR2(Context context, AudioManager audioManager, + View view, TransportMediatorCallback transportCallback) { + mContext = context; + mAudioManager = audioManager; + mTargetView = view; + mTransportCallback = transportCallback; + mReceiverAction = context.getPackageName() + ":transport:" + System.identityHashCode(this); + mIntent = new Intent(mReceiverAction); + mIntent.setPackage(context.getPackageName()); + mReceiverFilter = new IntentFilter(); + mReceiverFilter.addAction(mReceiverAction); + mTargetView.getViewTreeObserver().addOnWindowAttachListener(mWindowAttachListener); + mTargetView.getViewTreeObserver().addOnWindowFocusChangeListener(mWindowFocusListener); + } + + public Object getRemoteControlClient() { + return mRemoteControl; + } + + public void destroy() { + windowDetached(); + mTargetView.getViewTreeObserver().removeOnWindowAttachListener(mWindowAttachListener); + mTargetView.getViewTreeObserver().removeOnWindowFocusChangeListener(mWindowFocusListener); + } + + void windowAttached() { + mContext.registerReceiver(mMediaButtonReceiver, mReceiverFilter); + mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mIntent, + PendingIntent.FLAG_CANCEL_CURRENT); + mRemoteControl = new RemoteControlClient(mPendingIntent); + mRemoteControl.setOnGetPlaybackPositionListener(this); + mRemoteControl.setPlaybackPositionUpdateListener(this); + } + + void gainFocus() { + if (!mFocused) { + mFocused = true; + mAudioManager.registerMediaButtonEventReceiver(mPendingIntent); + mAudioManager.registerRemoteControlClient(mRemoteControl); + if (mPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { + takeAudioFocus(); + } + } + } + + void takeAudioFocus() { + if (!mAudioFocused) { + mAudioFocused = true; + mAudioManager.requestAudioFocus(mAudioFocusChangeListener, + AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + } + + public void startPlaying() { + if (mPlayState != RemoteControlClient.PLAYSTATE_PLAYING) { + mPlayState = RemoteControlClient.PLAYSTATE_PLAYING; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + if (mFocused) { + takeAudioFocus(); + } + } + + @Override + public long onGetPlaybackPosition() { + return mTransportCallback.getPlaybackPosition(); + } + + @Override + public void onPlaybackPositionUpdate(long newPositionMs) { + mTransportCallback.playbackPositionUpdate(newPositionMs); + } + + public void refreshState(boolean playing, long position, int transportControls) { + if (mRemoteControl != null) { + mRemoteControl.setPlaybackState(playing ? RemoteControlClient.PLAYSTATE_PLAYING + : RemoteControlClient.PLAYSTATE_STOPPED, position, playing ? 1 : 0); + mRemoteControl.setTransportControlFlags(transportControls); + } + } + + public void pausePlaying() { + if (mPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { + mPlayState = RemoteControlClient.PLAYSTATE_PAUSED; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + } + dropAudioFocus(); + } + + public void stopPlaying() { + if (mPlayState != RemoteControlClient.PLAYSTATE_STOPPED) { + mPlayState = RemoteControlClient.PLAYSTATE_STOPPED; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + } + dropAudioFocus(); + } + + void dropAudioFocus() { + if (mAudioFocused) { + mAudioFocused = false; + mAudioManager.abandonAudioFocus(mAudioFocusChangeListener); + } + } + + void loseFocus() { + dropAudioFocus(); + if (mFocused) { + mFocused = false; + mAudioManager.unregisterRemoteControlClient(mRemoteControl); + mAudioManager.unregisterMediaButtonEventReceiver(mPendingIntent); + } + } + + void windowDetached() { + loseFocus(); + if (mPendingIntent != null) { + mContext.unregisterReceiver(mMediaButtonReceiver); + mPendingIntent.cancel(); + mPendingIntent = null; + mRemoteControl = null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java new file mode 100644 index 00000000000..2d3afc9fe2f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java @@ -0,0 +1,202 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.os.SystemClock; +import android.view.KeyEvent; + +/** + * Implemented by the playback side of the media system, to respond to + * requests to perform actions and to retrieve its current state. These + * 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. + */ +public abstract class TransportPerformer { + /** + * Request to start playback on the media, resuming from whatever current state + * (position etc) it is in. + */ + 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. + */ + public abstract void onPause(); + + /** + * Request to completely stop playback of the media, clearing whatever state the + * player thinks is appropriate. + */ + public abstract void onStop(); + + /** + * Request to return the duration of the current media, in milliseconds. + */ + public abstract long onGetDuration(); + + /** + * Request to return the current playback position, in milliseconds. + */ + public abstract long onGetCurrentPosition(); + + /** + * Request to move the current playback position. + * @param pos New position to move to, in milliseconds. + */ + public abstract void onSeekTo(long pos); + + /** + * Request to find out whether the player is currently playing its media. + */ + public abstract boolean onIsPlaying(); + + /** + * Request to find out how much of the media has been buffered on the local device. + * @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. + */ + public int onGetBufferPercentage() { + return 100; + } + + /** + * Retrieves the flags for the media transport control buttons that this transport supports. + * Result is a combination of the following flags: + * {@link TransportMediator#FLAG_KEY_MEDIA_PREVIOUS}, + * {@link TransportMediator#FLAG_KEY_MEDIA_REWIND}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}, + * {@link TransportMediator#FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link TransportMediator#FLAG_KEY_MEDIA_NEXT} + * + *

The default implementation returns: + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, and + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}

+ */ + public int onGetTransportControlFlags() { + return TransportMediator.FLAG_KEY_MEDIA_PLAY + | TransportMediator.FLAG_KEY_MEDIA_PLAY_PAUSE + | TransportMediator.FLAG_KEY_MEDIA_PAUSE + | TransportMediator.FLAG_KEY_MEDIA_STOP; + } + + /** + * Report that a media button has been pressed. This is like + * {@link android.view.KeyEvent.Callback#onKeyDown(int, android.view.KeyEvent)} but + * will only deliver media keys. The default implementation handles these keys: + *
    + *
  • KEYCODE_MEDIA_PLAY: call {@link #onStart}
  • + *
  • KEYCODE_MEDIA_PAUSE: call {@link #onPause}
  • + *
  • KEYCODE_MEDIA_STOP: call {@link #onStop}
  • + *
  • KEYCODE_MEDIA_PLAY_PAUSE and KEYCODE_HEADSETHOOK: call {@link #onPause} + * if {@link #onIsPlaying()} returns true, otherwise call {@link #onStart}
  • + *
+ * @param keyCode The code of the media key. + * @param event The full key event. + * @return Indicate whether the key has been consumed. The default + * implementation always returns true. This only matters for keys + * being dispatched here from + * {@link TransportMediator#dispatchKeyEvent(android.view.KeyEvent) + * TransportController.dispatchKeyEvent}, and determines whether the key + * continues on to its default key handling (which for media keys means + * being delivered to the current media remote control, which should + * be us). + */ + public boolean onMediaButtonDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case TransportMediator.KEYCODE_MEDIA_PLAY: + onStart(); + return true; + case TransportMediator.KEYCODE_MEDIA_PAUSE: + onPause(); + return true; + case KeyEvent.KEYCODE_MEDIA_STOP: + onStop(); + return true; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + if (onIsPlaying()) { + onPause(); + } else { + onStart(); + } + } + return true; + } + + /** + * Report that a media button has been released. This is like + * {@link KeyEvent.Callback#onKeyUp(int, android.view.KeyEvent)} but + * will only deliver media keys. The default implementation does nothing. + * @param keyCode The code of the media key. + * @param event The full key event. + * @return Indicate whether the key has been consumed. The default + * implementation always returns true. This only matters for keys + * being dispatched here from + * {@link TransportMediator#dispatchKeyEvent(android.view.KeyEvent) + * TransportController.dispatchKeyEvent}, and determines whether the key + * continues on to its default key handling (which for media keys means + * being delivered to the current media remote control, which should + * be us). + */ + public boolean onMediaButtonUp(int keyCode, KeyEvent event) { + return true; + } + + // Copy constants from framework since we can't link to them. + static final int AUDIOFOCUS_GAIN = 1; + static final int AUDIOFOCUS_GAIN_TRANSIENT = 2; + static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = 3; + static final int AUDIOFOCUS_LOSS = -1 * AUDIOFOCUS_GAIN; + static final int AUDIOFOCUS_LOSS_TRANSIENT = -1 * AUDIOFOCUS_GAIN_TRANSIENT; + static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = + -1 * AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + /** + * Report that audio focus has changed on the app. This only happens if + * you have indicated you have started playing with + * {@link TransportMediator#startPlaying TransportController.startPlaying}, + * which takes audio focus for you. + * @param focusChange The type of focus change, as per + * {@link android.media.AudioManager.OnAudioFocusChangeListener#onAudioFocusChange(int) + * OnAudioFocusChangeListener.onAudioFocusChange}. The default implementation will + * deliver a {@link KeyEvent#KEYCODE_MEDIA_STOP} + * when receiving {@link android.media.AudioManager#AUDIOFOCUS_LOSS}. + */ + public void onAudioFocusChange(int focusChange) { + int keyCode = 0; + switch (focusChange) { + case AUDIOFOCUS_LOSS: + // This will cause us to stop playback, which means we drop audio focus + // so we will not get any further audio focus gain. + keyCode = TransportMediator.KEYCODE_MEDIA_PAUSE; + break; + } + if (keyCode != 0) { + final long now = SystemClock.uptimeMillis(); + onMediaButtonDown(keyCode, new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0)); + onMediaButtonUp(keyCode, new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0)); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java new file mode 100644 index 00000000000..384cca26010 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package android.support.v4.media; + +/** + * A listener for playback changes that can be registered with + * {@link TransportController}. + */ +public class TransportStateListener { + /** + * The play state of the transport changed. Use + * {@link android.support.v4.media.TransportController#isPlaying() + * TransportController.isPlaying()} to determine the new state. + */ + public void onPlayingChanged(TransportController controller) { + } + + /** + * The available controls of the transport changed. Use + * {@link TransportController#getTransportControlFlags()} + * TransportController.getTransportControlFlags()} to determine the new state. + */ + public void onTransportControlsChanged(TransportController controller) { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java new file mode 100644 index 00000000000..2efa7a611d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java @@ -0,0 +1,171 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.os.Build; +import android.support.v4.media.session.MediaSessionCompat; + +/** + * Handles requests to adjust or set the volume on a session. This is also used + * to push volume updates back to the session after a request has been handled. + * You can set a volume provider on a session by calling + * {@link MediaSessionCompat#setPlaybackToRemote}. + */ +public abstract class VolumeProviderCompat { + /** + * The volume is fixed and can not be modified. Requests to change volume + * should be ignored. + */ + public static final int VOLUME_CONTROL_FIXED = 0; + + /** + * The volume control uses relative adjustment via + * {@link #onAdjustVolume(int)}. Attempts to set the volume to a specific + * value should be ignored. + */ + public static final int VOLUME_CONTROL_RELATIVE = 1; + + /** + * The volume control uses an absolute value. It may be adjusted using + * {@link #onAdjustVolume(int)} or set directly using + * {@link #onSetVolumeTo(int)}. + */ + public static final int VOLUME_CONTROL_ABSOLUTE = 2; + + private final int mControlType; + private final int mMaxVolume; + private int mCurrentVolume; + private Callback mCallback; + + private Object mVolumeProviderObj; + + /** + * Create a new volume provider for handling volume events. You must specify + * the type of volume control and the maximum volume that can be used. + * + * @param volumeControl The method for controlling volume that is used by + * this provider. + * @param maxVolume The maximum allowed volume. + * @param currentVolume The current volume. + */ + public VolumeProviderCompat(int volumeControl, int maxVolume, int currentVolume) { + mControlType = volumeControl; + mMaxVolume = maxVolume; + mCurrentVolume = currentVolume; + } + + /** + * Get the current volume of the provider. + * + * @return The current volume. + */ + public final int getCurrentVolume() { + return mCurrentVolume; + } + + /** + * Get the volume control type that this volume provider uses. + * + * @return The volume control type for this volume provider + */ + public final int getVolumeControl() { + return mControlType; + } + + /** + * Get the maximum volume this provider allows. + * + * @return The max allowed volume. + */ + public final int getMaxVolume() { + return mMaxVolume; + } + + /** + * Set the current volume and notify the system that the volume has been + * changed. + * + * @param currentVolume The current volume of the output. + */ + public final void setCurrentVolume(int currentVolume) { + if (mCallback != null) { + mCallback.onVolumeChanged(this); + } + } + + /** + * Override to handle requests to set the volume of the current output. + * + * @param volume The volume to set the output to. + */ + public void onSetVolumeTo(int volume) { + } + + /** + * Override to handle requests to adjust the volume of the current output. + * + * @param direction The direction to adjust the volume in. + */ + public void onAdjustVolume(int direction) { + } + + /** + * Sets a callback to receive volume changes. + *

+ * Used internally by the support library. + *

+ */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * Gets the underlying framework {@link android.media.VolumeProvider} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return An equivalent {@link android.media.VolumeProvider} object, or null if none. + */ + public Object getVolumeProvider() { + if (mVolumeProviderObj != null || Build.VERSION.SDK_INT < 21) { + return mVolumeProviderObj; + } + + mVolumeProviderObj = VolumeProviderCompatApi21.createVolumeProvider( + mControlType, mMaxVolume, mCurrentVolume, new VolumeProviderCompatApi21.Delegate() { + + @Override + public void onSetVolumeTo(int volume) { + VolumeProviderCompat.this.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + VolumeProviderCompat.this.onAdjustVolume(direction); + } + }); + return mVolumeProviderObj; + } + + /** + * Listens for changes to the volume. + */ + public static abstract class Callback { + public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java new file mode 100644 index 00000000000..c0f21c5f2da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +package android.support.v4.media; + +import android.media.VolumeProvider; + +class VolumeProviderCompatApi21 { + public static Object createVolumeProvider(int volumeControl, int maxVolume, int currentVolume, + final Delegate delegate) { + return new VolumeProvider(volumeControl, maxVolume, currentVolume) { + @Override + public void onSetVolumeTo(int volume) { + delegate.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + delegate.onAdjustVolume(direction); + } + }; + } + + public static void setCurrentVolume(Object volumeProviderObj, int currentVolume) { + ((VolumeProvider) volumeProviderObj).setCurrentVolume(currentVolume); + } + + public interface Delegate { + void onSetVolumeTo(int volume); + void onAdjustVolume(int delta); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java new file mode 100644 index 00000000000..3cf67271e42 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java @@ -0,0 +1,442 @@ +/* + * 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. + */ + +package android.support.v4.media.routing; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +class MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybean"; + + public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + public static final int ROUTE_TYPE_USER = 0x00800000; + + public static final int ALL_ROUTE_TYPES = + MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO + | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO + | MediaRouterJellybean.ROUTE_TYPE_USER; + + public static Object getMediaRouter(Context context) { + return context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getRouteAt(i)); + } + return out; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getCategories(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getCategoryCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getCategoryAt(i)); + } + return out; + } + + public static Object getSelectedRoute(Object routerObj, int type) { + return ((android.media.MediaRouter)routerObj).getSelectedRoute(type); + } + + public static void selectRoute(Object routerObj, int types, Object routeObj) { + ((android.media.MediaRouter)routerObj).selectRoute(types, + (android.media.MediaRouter.RouteInfo)routeObj); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj); + } + + public static void removeCallback(Object routerObj, Object callbackObj) { + ((android.media.MediaRouter)routerObj).removeCallback( + (android.media.MediaRouter.Callback)callbackObj); + } + + public static Object createRouteCategory(Object routerObj, + String name, boolean isGroupable) { + return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable); + } + + public static Object createUserRoute(Object routerObj, Object categoryObj) { + return ((android.media.MediaRouter)routerObj).createUserRoute( + (android.media.MediaRouter.RouteCategory)categoryObj); + } + + public static void addUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).addUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static void removeUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).removeUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static Object createVolumeCallback(VolumeCallback callback) { + return new VolumeCallbackProxy(callback); + } + + public static final class RouteInfo { + public static CharSequence getName(Object routeObj, Context context) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context); + } + + public static CharSequence getStatus(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus(); + } + + public static int getSupportedTypes(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes(); + } + + public static Object getCategory(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory(); + } + + public static Drawable getIconDrawable(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable(); + } + + public static int getPlaybackType(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType(); + } + + public static int getPlaybackStream(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream(); + } + + public static int getVolume(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume(); + } + + public static int getVolumeMax(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax(); + } + + public static int getVolumeHandling(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling(); + } + + public static Object getTag(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getTag(); + } + + public static void setTag(Object routeObj, Object tag) { + ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag); + } + + public static void requestSetVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume); + } + + public static void requestUpdateVolume(Object routeObj, int direction) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction); + } + + public static Object getGroup(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup(); + } + + public static boolean isGroup(Object routeObj) { + return routeObj instanceof android.media.MediaRouter.RouteGroup; + } + } + + public static final class RouteGroup { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getGroupedRoutes(Object groupObj) { + final android.media.MediaRouter.RouteGroup group = + (android.media.MediaRouter.RouteGroup)groupObj; + final int count = group.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(group.getRouteAt(i)); + } + return out; + } + } + + public static final class UserRouteInfo { + public static void setName(Object routeObj, CharSequence name) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name); + } + + public static void setStatus(Object routeObj, CharSequence status) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status); + } + + public static void setIconDrawable(Object routeObj, Drawable icon) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon); + } + + public static void setPlaybackType(Object routeObj, int type) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type); + } + + public static void setPlaybackStream(Object routeObj, int stream) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream); + } + + public static void setVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume); + } + + public static void setVolumeMax(Object routeObj, int volumeMax) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax); + } + + public static void setVolumeHandling(Object routeObj, int volumeHandling) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling); + } + + public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback( + (android.media.MediaRouter.VolumeCallback)volumeCallbackObj); + } + + public static void setRemoteControlClient(Object routeObj, Object rccObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient( + (android.media.RemoteControlClient)rccObj); + } + } + + public static final class RouteCategory { + public static CharSequence getName(Object categoryObj, Context context) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object categoryObj) { + ArrayList out = new ArrayList(); + ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out); + return out; + } + + public static int getSupportedTypes(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes(); + } + + public static boolean isGroupable(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable(); + } + } + + public static interface Callback { + public void onRouteSelected(int type, Object routeObj); + public void onRouteUnselected(int type, Object routeObj); + public void onRouteAdded(Object routeObj); + public void onRouteRemoved(Object routeObj); + public void onRouteChanged(Object routeObj); + public void onRouteGrouped(Object routeObj, Object groupObj, int index); + public void onRouteUngrouped(Object routeObj, Object groupObj); + public void onRouteVolumeChanged(Object routeObj); + } + + public static interface VolumeCallback { + public void onVolumeSetRequest(Object routeObj, int volume); + public void onVolumeUpdateRequest(Object routeObj, int direction); + } + + /** + * Workaround for limitations of selectRoute() on JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class SelectRouteWorkaround { + private Method mSelectRouteIntMethod; + + public SelectRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod( + "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class); + } catch (NoSuchMethodException ex) { + } + } + + public void selectRoute(Object routerObj, int types, Object routeObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + int routeTypes = route.getSupportedTypes(); + if ((routeTypes & ROUTE_TYPE_USER) == 0) { + // Handle non-user routes. + // On JB and JB MR1, the selectRoute() API only supports programmatically + // selecting user routes. So instead we rely on the hidden selectRouteInt() + // method on these versions of the platform. + // This limitation was removed in JB MR2. + if (mSelectRouteIntMethod != null) { + try { + mSelectRouteIntMethod.invoke(router, types, route); + return; // success! + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } + } else { + Log.w(TAG, "Cannot programmatically select non-user route " + + "because the platform is missing the selectRouteInt() " + + "method. Media routing may not work."); + } + } + + // Default handling. + router.selectRoute(types, route); + } + } + + /** + * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class GetDefaultRouteWorkaround { + private Method mGetSystemAudioRouteMethod; + + public GetDefaultRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mGetSystemAudioRouteMethod = + android.media.MediaRouter.class.getMethod("getSystemAudioRoute"); + } catch (NoSuchMethodException ex) { + } + } + + public Object getDefaultRoute(Object routerObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + + if (mGetSystemAudioRouteMethod != null) { + try { + return mGetSystemAudioRouteMethod.invoke(router); + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Could not find the method or it does not work. + // Return the first route and hope for the best. + return router.getRouteAt(0); + } + } + + static class CallbackProxy + extends android.media.MediaRouter.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onRouteSelected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteSelected(type, route); + } + + @Override + public void onRouteUnselected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteUnselected(type, route); + } + + @Override + public void onRouteAdded(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteAdded(route); + } + + @Override + public void onRouteRemoved(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteRemoved(route); + } + + @Override + public void onRouteChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteChanged(route); + } + + @Override + public void onRouteGrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group, int index) { + mCallback.onRouteGrouped(route, group, index); + } + + @Override + public void onRouteUngrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group) { + mCallback.onRouteUngrouped(route, group); + } + + @Override + public void onRouteVolumeChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteVolumeChanged(route); + } + } + + static class VolumeCallbackProxy + extends android.media.MediaRouter.VolumeCallback { + protected final T mCallback; + + public VolumeCallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route, + int volume) { + mCallback.onVolumeSetRequest(route, volume); + } + + @Override + public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route, + int direction) { + mCallback.onVolumeUpdateRequest(route, direction); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java new file mode 100644 index 00000000000..6e5cfd5d1ab --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java @@ -0,0 +1,176 @@ +/* + * 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. + */ + +package android.support.v4.media.routing; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.view.Display; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class MediaRouterJellybeanMr1 extends MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybeanMr1"; + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static final class RouteInfo { + public static boolean isEnabled(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled(); + } + + public static Display getPresentationDisplay(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay(); + } + } + + public static interface Callback extends MediaRouterJellybean.Callback { + public void onRoutePresentationDisplayChanged(Object routeObj); + } + + /** + * Workaround the fact that the version of MediaRouter.addCallback() that accepts a + * flag to perform an active scan does not exist in JB MR1 so we need to force + * wifi display scans directly through the DisplayManager. + * Do not use on JB MR2 and above. + */ + public static final class ActiveScanWorkaround implements Runnable { + // Time between wifi display scans when actively scanning in milliseconds. + private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000; + + private final DisplayManager mDisplayManager; + private final Handler mHandler; + private Method mScanWifiDisplaysMethod; + + private boolean mActivelyScanningWifiDisplays; + + public ActiveScanWorkaround(Context context, Handler handler) { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + mHandler = handler; + try { + mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays"); + } catch (NoSuchMethodException ex) { + } + } + + public void setActiveScanRouteTypes(int routeTypes) { + // On JB MR1, there is no API to scan wifi display routes. + // Instead we must make a direct call into the DisplayManager to scan + // wifi displays on this version but only when live video routes are requested. + // See also the JellybeanMr2Impl implementation of this method. + // This was fixed in JB MR2 by adding a new overload of addCallback() to + // enable active scanning on request. + if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) { + if (!mActivelyScanningWifiDisplays) { + if (mScanWifiDisplaysMethod != null) { + mActivelyScanningWifiDisplays = true; + mHandler.post(this); + } else { + Log.w(TAG, "Cannot scan for wifi displays because the " + + "DisplayManager.scanWifiDisplays() method is " + + "not available on this device."); + } + } + } else { + if (mActivelyScanningWifiDisplays) { + mActivelyScanningWifiDisplays = false; + mHandler.removeCallbacks(this); + } + } + } + + @Override + public void run() { + if (mActivelyScanningWifiDisplays) { + try { + mScanWifiDisplaysMethod.invoke(mDisplayManager); + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } + mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL); + } + } + } + + /** + * Workaround the fact that the isConnecting() method does not exist in JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class IsConnectingWorkaround { + private Method mGetStatusCodeMethod; + private int mStatusConnecting; + + public IsConnectingWorkaround() { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + try { + Field statusConnectingField = + android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING"); + mStatusConnecting = statusConnectingField.getInt(null); + mGetStatusCodeMethod = + android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode"); + } catch (NoSuchFieldException ex) { + } catch (NoSuchMethodException ex) { + } catch (IllegalAccessException ex) { + } + } + + public boolean isConnecting(Object routeObj) { + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + if (mGetStatusCodeMethod != null) { + try { + int statusCode = (Integer)mGetStatusCodeMethod.invoke(route); + return statusCode == mStatusConnecting; + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Assume not connecting. + return false; + } + } + + static class CallbackProxy + extends MediaRouterJellybean.CallbackProxy { + public CallbackProxy(T callback) { + super(callback); + } + + @Override + public void onRoutePresentationDisplayChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRoutePresentationDisplayChanged(route); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java new file mode 100644 index 00000000000..92a1607a01e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package android.support.v4.media.routing; + +class MediaRouterJellybeanMr2 extends MediaRouterJellybeanMr1 { + public static Object getDefaultRoute(Object routerObj) { + return ((android.media.MediaRouter)routerObj).getDefaultRoute(); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj, flags); + } + + public static final class RouteInfo { + public static CharSequence getDescription(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription(); + } + + public static boolean isConnecting(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting(); + } + } + + public static final class UserRouteInfo { + public static void setDescription(Object routeObj, CharSequence description) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl new file mode 100644 index 00000000000..d905350a3af --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl @@ -0,0 +1,40 @@ +/* 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. + */ + +package android.support.v4.media.session; + +import android.os.Bundle; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.ParcelableVolumeInfo; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.MediaSessionCompat; + +/** + * Callback interface for a MediaSessionCompat to send updates to a + * MediaControllerCompat. This is only used on pre-Lollipop systems. + * @hide + */ +oneway interface IMediaControllerCallback { + void onEvent(String event, in Bundle extras); + void onSessionDestroyed(); + + // These callbacks are for the TransportController + void onPlaybackStateChanged(in PlaybackStateCompat state); + void onMetadataChanged(in MediaMetadataCompat metadata); + void onQueueChanged(in List queue); + void onQueueTitleChanged(CharSequence title); + void onExtrasChanged(in Bundle extras); + void onVolumeInfoChanged(in ParcelableVolumeInfo info); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl new file mode 100644 index 00000000000..1ce425dc4ef --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl @@ -0,0 +1,69 @@ +/* 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. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Intent; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.IMediaControllerCallback; +import android.support.v4.media.session.ParcelableVolumeInfo; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.os.Bundle; +import android.view.KeyEvent; + +import java.util.List; + +/** + * Interface to a MediaSessionCompat. This is only used on pre-Lollipop systems. + * @hide + */ +interface IMediaSession { + void sendCommand(String command, in Bundle args, in MediaSessionCompat.ResultReceiverWrapper cb); + boolean sendMediaButton(in KeyEvent mediaButton); + void registerCallbackListener(in IMediaControllerCallback cb); + void unregisterCallbackListener(in IMediaControllerCallback cb); + boolean isTransportControlEnabled(); + String getPackageName(); + String getTag(); + PendingIntent getLaunchPendingIntent(); + long getFlags(); + ParcelableVolumeInfo getVolumeAttributes(); + void adjustVolume(int direction, int flags, String packageName); + void setVolumeTo(int value, int flags, String packageName); + + // These commands are for the TransportControls + void play(); + void playFromMediaId(String uri, in Bundle extras); + void playFromSearch(String string, in Bundle extras); + void skipToQueueItem(long id); + void pause(); + void stop(); + void next(); + void previous(); + void fastForward(); + void rewind(); + void seekTo(long pos); + void rate(in RatingCompat rating); + void sendCustomAction(String action, in Bundle args); + MediaMetadataCompat getMetadata(); + PlaybackStateCompat getPlaybackState(); + List getQueue(); + CharSequence getQueueTitle(); + Bundle getExtras(); + int getRatingType(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java new file mode 100644 index 00000000000..ef99f43ee25 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java @@ -0,0 +1,1323 @@ +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.VolumeProviderCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat.CustomAction; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Allows an app to interact with an ongoing media session. Media buttons and + * other commands can be sent to the session. A callback may be registered to + * receive updates from the session, such as metadata and play state changes. + *

+ * A MediaController can be created if you have a {@link MediaSessionCompat.Token} + * from the session owner. + *

+ * MediaController objects are thread-safe. + *

+ * This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + */ +public final class MediaControllerCompat { + private static final String TAG = "MediaControllerCompat"; + + private final MediaControllerImpl mImpl; + private final MediaSessionCompat.Token mToken; + + /** + * Creates a media controller from a session. + * + * @param session The session to be controlled. + */ + public MediaControllerCompat(Context context, MediaSessionCompat session) { + if (session == null) { + throw new IllegalArgumentException("session must not be null"); + } + mToken = session.getSessionToken(); + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, session); + } else { + mImpl = new MediaControllerImplBase(mToken); + } + } + + /** + * Creates a media controller from a session token which may have + * been obtained from another process. + * + * @param sessionToken The token of the session to be controlled. + * @throws RemoteException if the session is not accessible. + */ + public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + if (sessionToken == null) { + throw new IllegalArgumentException("sessionToken must not be null"); + } + mToken = sessionToken; + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, sessionToken); + } else { + mImpl = new MediaControllerImplBase(mToken); + } + } + + /** + * Get a {@link TransportControls} instance for this session. + * + * @return A controls instance + */ + public TransportControls getTransportControls() { + return mImpl.getTransportControls(); + } + + /** + * Send the specified media button event to the session. Only media keys can + * be sent by this method, other keys will be ignored. + * + * @param keyEvent The media button event to dispatch. + * @return true if the event was sent to the session, false otherwise. + */ + public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) { + if (keyEvent == null) { + throw new IllegalArgumentException("KeyEvent may not be null"); + } + return mImpl.dispatchMediaButtonEvent(keyEvent); + } + + /** + * Get the current playback state for this session. + * + * @return The current PlaybackState or null + */ + public PlaybackStateCompat getPlaybackState() { + return mImpl.getPlaybackState(); + } + + /** + * Get the current metadata for this session. + * + * @return The current MediaMetadata or null. + */ + public MediaMetadataCompat getMetadata() { + return mImpl.getMetadata(); + } + + /** + * Get the current play queue for this session if one is set. If you only + * care about the current item {@link #getMetadata()} should be used. + * + * @return The current play queue or null. + */ + public List getQueue() { + return mImpl.getQueue(); + } + + /** + * Get the queue title for this session. + */ + public CharSequence getQueueTitle() { + return mImpl.getQueueTitle(); + } + + /** + * Get the extras for this session. + */ + public Bundle getExtras() { + return mImpl.getExtras(); + } + + /** + * Get the rating type supported by the session. One of: + *

    + *
  • {@link RatingCompat#RATING_NONE}
  • + *
  • {@link RatingCompat#RATING_HEART}
  • + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN}
  • + *
  • {@link RatingCompat#RATING_3_STARS}
  • + *
  • {@link RatingCompat#RATING_4_STARS}
  • + *
  • {@link RatingCompat#RATING_5_STARS}
  • + *
  • {@link RatingCompat#RATING_PERCENTAGE}
  • + *
+ * + * @return The supported rating type + */ + public int getRatingType() { + return mImpl.getRatingType(); + } + + /** + * Get the flags for this session. Flags are defined in + * {@link MediaSessionCompat}. + * + * @return The current set of flags for the session. + */ + public long getFlags() { + return mImpl.getFlags(); + } + + /** + * Get the current playback info for this session. + * + * @return The current playback info or null. + */ + public PlaybackInfo getPlaybackInfo() { + return mImpl.getPlaybackInfo(); + } + + /** + * Get an intent for launching UI associated with this session if one + * exists. + * + * @return A {@link PendingIntent} to launch UI or null. + */ + public PendingIntent getSessionActivity() { + return mImpl.getSessionActivity(); + } + + /** + * Get the token for the session this controller is connected to. + * + * @return The session's token. + */ + public MediaSessionCompat.Token getSessionToken() { + return mToken; + } + + /** + * Set the volume of the output this session is playing on. The command will + * be ignored if it does not support + * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in + * {@link AudioManager} may be used to affect the handling. + * + * @see #getPlaybackInfo() + * @param value The value to set it to, between 0 and the reported max. + * @param flags Flags from {@link AudioManager} to include with the volume + * request. + */ + public void setVolumeTo(int value, int flags) { + mImpl.setVolumeTo(value, flags); + } + + /** + * Adjust the volume of the output this session is playing on. The direction + * must be one of {@link AudioManager#ADJUST_LOWER}, + * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. + * The command will be ignored if the session does not support + * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or + * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in + * {@link AudioManager} may be used to affect the handling. + * + * @see #getPlaybackInfo() + * @param direction The direction to adjust the volume in. + * @param flags Any flags to pass with the command. + */ + public void adjustVolume(int direction, int flags) { + mImpl.adjustVolume(direction, flags); + } + + /** + * Adds a callback to receive updates from the Session. Updates will be + * posted on the caller's thread. + * + * @param callback The callback object, must not be null. + */ + public void registerCallback(Callback callback) { + registerCallback(callback, null); + } + + /** + * Adds a callback to receive updates from the session. Updates will be + * posted on the specified handler's thread. + * + * @param callback The callback object, must not be null. + * @param handler The handler to post updates on. If null the callers thread + * will be used. + */ + public void registerCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + handler = new Handler(); + } + mImpl.registerCallback(callback, handler); + } + + /** + * Stop receiving updates on the specified callback. If an update has + * already been posted you may still receive it after calling this method. + * + * @param callback The callback to remove + */ + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + mImpl.unregisterCallback(callback); + } + + /** + * Sends a generic command to the session. It is up to the session creator + * to decide what commands and parameters they will support. As such, + * commands should only be sent to sessions that the controller owns. + * + * @param command The command to send + * @param params Any parameters to include with the command + * @param cb The callback to receive the result on + */ + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command cannot be null or empty"); + } + mImpl.sendCommand(command, params, cb); + } + + /** + * Get the session owner's package name. + * + * @return The package name of of the session owner. + */ + public String getPackageName() { + return mImpl.getPackageName(); + } + + /** + * Gets the underlying framework + * {@link android.media.session.MediaController} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaController} + * object, or null if none. + */ + public Object getMediaController() { + return mImpl.getMediaController(); + } + + /** + * Callback for receiving updates on from the session. A Callback can be + * registered using {@link #registerCallback} + */ + public static abstract class Callback implements IBinder.DeathRecipient { + private final Object mCallbackObj; + private MessageHandler mHandler; + + private boolean mRegistered = false; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = new StubCompat(); + } + } + + /** + * Override to handle the session being destroyed. The session is no + * longer valid after this call and calls to it will be ignored. + */ + public void onSessionDestroyed() { + } + + /** + * Override to handle custom events sent by the session owner without a + * specified interface. Controllers should only handle these for + * sessions they own. + * + * @param event The event from the session. + * @param extras Optional parameters for the event. + */ + public void onSessionEvent(String event, Bundle extras) { + } + + /** + * Override to handle changes in playback state. + * + * @param state The new playback state of the session + */ + public void onPlaybackStateChanged(PlaybackStateCompat state) { + } + + /** + * Override to handle changes to the current metadata. + * + * @param metadata The current metadata for the session or null if none. + * @see MediaMetadata + */ + public void onMetadataChanged(MediaMetadataCompat metadata) { + } + + /** + * Override to handle changes to items in the queue. + * + * @see MediaSessionCompat.QueueItem + * @param queue A list of items in the current play queue. It should + * include the currently playing item as well as previous and + * upcoming items if applicable. + */ + public void onQueueChanged(List queue) { + } + + /** + * Override to handle changes to the queue title. + * + * @param title The title that should be displayed along with the play + * queue such as "Now Playing". May be null if there is no + * such title. + */ + public void onQueueTitleChanged(CharSequence title) { + } + + /** + * Override to handle chagnes to the {@link MediaSessionCompat} extras. + * + * @param extras The extras that can include other information + * associated with the {@link MediaSessionCompat}. + */ + public void onExtrasChanged(Bundle extras) { + } + + /** + * Override to handle changes to the audio info. + * + * @param info The current audio info for this session. + */ + public void onAudioInfoChanged(PlaybackInfo info) { + } + + @Override + public void binderDied() { + onSessionDestroyed(); + } + + /** + * Set the handler to use for pre 21 callbacks. + */ + private void setHandler(Handler handler) { + mHandler = new MessageHandler(handler.getLooper()); + } + + private class StubApi21 implements MediaControllerCompatApi21.Callback { + @Override + public void onSessionDestroyed() { + Callback.this.onSessionDestroyed(); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + Callback.this.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(Object stateObj) { + Callback.this.onPlaybackStateChanged( + PlaybackStateCompat.fromPlaybackState(stateObj)); + } + + @Override + public void onMetadataChanged(Object metadataObj) { + Callback.this.onMetadataChanged( + MediaMetadataCompat.fromMediaMetadata(metadataObj)); + } + } + + private class StubCompat extends IMediaControllerCallback.Stub { + + @Override + public void onEvent(String event, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_EVENT, event, extras); + } + + @Override + public void onSessionDestroyed() throws RemoteException { + mHandler.post(MessageHandler.MSG_DESTROYED, null, null); + } + + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null); + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null); + } + + @Override + public void onQueueChanged(List queue) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null); + } + + @Override + public void onQueueTitleChanged(CharSequence title) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null); + } + + @Override + public void onExtrasChanged(Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null); + } + + @Override + public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException { + PlaybackInfo pi = null; + if (info != null) { + pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType, + info.maxVolume, info.currentVolume); + } + mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, null); + } + } + + private class MessageHandler extends Handler { + private static final int MSG_EVENT = 1; + private static final int MSG_UPDATE_PLAYBACK_STATE = 2; + private static final int MSG_UPDATE_METADATA = 3; + private static final int MSG_UPDATE_VOLUME = 4; + private static final int MSG_UPDATE_QUEUE = 5; + private static final int MSG_UPDATE_QUEUE_TITLE = 6; + private static final int MSG_UPDATE_EXTRAS = 7; + private static final int MSG_DESTROYED = 8; + + public MessageHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (!mRegistered) { + return; + } + switch (msg.what) { + case MSG_EVENT: + onSessionEvent((String) msg.obj, msg.getData()); + break; + case MSG_UPDATE_PLAYBACK_STATE: + onPlaybackStateChanged((PlaybackStateCompat) msg.obj); + break; + case MSG_UPDATE_METADATA: + onMetadataChanged((MediaMetadataCompat) msg.obj); + break; + case MSG_UPDATE_QUEUE: + onQueueChanged((List) msg.obj); + break; + case MSG_UPDATE_QUEUE_TITLE: + onQueueTitleChanged((CharSequence) msg.obj); + break; + case MSG_UPDATE_EXTRAS: + onExtrasChanged((Bundle) msg.obj); + break; + case MSG_UPDATE_VOLUME: + onAudioInfoChanged((PlaybackInfo) msg.obj); + break; + case MSG_DESTROYED: + onSessionDestroyed(); + break; + } + } + + public void post(int what, Object obj, Bundle data) { + obtainMessage(what, obj).sendToTarget(); + } + } + } + + /** + * Interface for controlling media playback on a session. This allows an app + * to send media transport commands to the session. + */ + public static abstract class TransportControls { + TransportControls() { + } + + /** + * Request that the player start its playback at its current position. + */ + public abstract void play(); + + /** + * Request that the player start playback for a specific {@link Uri}. + * + * @param mediaId The uri of the requested media. + * @param extras Optional extras that can include extra information + * about the media item to be played. + */ + public abstract void playFromMediaId(String mediaId, Bundle extras); + + /** + * Request that the player start playback for a specific search query. + * An empty or null query should be treated as a request to play any + * music. + * + * @param query The search query. + * @param extras Optional extras that can include extra information + * about the query. + */ + public abstract void playFromSearch(String query, Bundle extras); + + /** + * Play an item with a specific id in the play queue. If you specify an + * id that is not in the play queue, the behavior is undefined. + */ + public abstract void skipToQueueItem(long id); + + /** + * Request that the player pause its playback and stay at its current + * position. + */ + public abstract void pause(); + + /** + * Request that the player stop its playback; it may clear its state in + * whatever way is appropriate. + */ + public abstract void stop(); + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Start fast forwarding. If playback is already fast forwarding this + * may increase the rate. + */ + public abstract void fastForward(); + + /** + * Skip to the next item. + */ + public abstract void skipToNext(); + + /** + * Start rewinding. If playback is already rewinding this may increase + * the rate. + */ + public abstract void rewind(); + + /** + * Skip to the previous item. + */ + public abstract void skipToPrevious(); + + /** + * Rate the current content. This will cause the rating to be set for + * the current user. The Rating type must match the type returned by + * {@link #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public abstract void setRating(RatingCompat rating); + + /** + * Send a custom action for the {@link MediaSessionCompat} to perform. + * + * @param customAction The action to perform. + * @param args Optional arguments to supply to the + * {@link MediaSessionCompat} for this custom action. + */ + public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction, + Bundle args); + + /** + * Send the id and args from a custom action for the + * {@link MediaSessionCompat} to perform. + * + * @see #sendCustomAction(PlaybackStateCompat.CustomAction action, + * Bundle args) + * @param action The action identifier of the + * {@link PlaybackStateCompat.CustomAction} as specified by + * the {@link MediaSessionCompat}. + * @param args Optional arguments to supply to the + * {@link MediaSessionCompat} for this custom action. + */ + public abstract void sendCustomAction(String action, Bundle args); + } + + /** + * Holds information about the way volume is handled for this session. + */ + public static final class PlaybackInfo { + /** + * The session uses local playback. + */ + public static final int PLAYBACK_TYPE_LOCAL = 1; + /** + * The session uses remote playback. + */ + public static final int PLAYBACK_TYPE_REMOTE = 2; + + private final int mPlaybackType; + // TODO update audio stream with AudioAttributes support version + private final int mAudioStream; + private final int mVolumeControl; + private final int mMaxVolume; + private final int mCurrentVolume; + + PlaybackInfo(int type, int stream, int control, int max, int current) { + mPlaybackType = type; + mAudioStream = stream; + mVolumeControl = control; + mMaxVolume = max; + mCurrentVolume = current; + } + + /** + * Get the type of volume handling, either local or remote. One of: + *
    + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}
  • + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}
  • + *
+ * + * @return The type of volume handling this session is using. + */ + public int getPlaybackType() { + return mPlaybackType; + } + + /** + * Get the stream this is currently controlling volume on. When the volume + * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not + * have meaning and should be ignored. + * + * @return The stream this session is playing on. + */ + public int getAudioStream() { + // TODO switch to AudioAttributesCompat when it is added. + return mAudioStream; + } + + /** + * Get the type of volume control that can be used. One of: + *
    + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}
  • + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}
  • + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}
  • + *
+ * + * @return The type of volume control that may be used with this + * session. + */ + public int getVolumeControl() { + return mVolumeControl; + } + + /** + * Get the maximum volume that may be set for this session. + * + * @return The maximum allowed volume where this session is playing. + */ + public int getMaxVolume() { + return mMaxVolume; + } + + /** + * Get the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mCurrentVolume; + } + } + + interface MediaControllerImpl { + void registerCallback(Callback callback, Handler handler); + + void unregisterCallback(Callback callback); + boolean dispatchMediaButtonEvent(KeyEvent keyEvent); + TransportControls getTransportControls(); + PlaybackStateCompat getPlaybackState(); + MediaMetadataCompat getMetadata(); + + List getQueue(); + CharSequence getQueueTitle(); + Bundle getExtras(); + int getRatingType(); + long getFlags(); + PlaybackInfo getPlaybackInfo(); + PendingIntent getSessionActivity(); + + void setVolumeTo(int value, int flags); + void adjustVolume(int direction, int flags); + void sendCommand(String command, Bundle params, ResultReceiver cb); + + String getPackageName(); + Object getMediaController(); + } + + static class MediaControllerImplBase implements MediaControllerImpl { + private MediaSessionCompat.Token mToken; + private IMediaSession mBinder; + private TransportControls mTransportControls; + + public MediaControllerImplBase(MediaSessionCompat.Token token) { + mToken = token; + mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken()); + } + + @Override + public void registerCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.asBinder().linkToDeath(callback, 0); + mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj); + callback.setHandler(handler); + callback.mRegistered = true; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerCallback. " + e); + callback.onSessionDestroyed(); + } + } + + @Override + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.unregisterCallbackListener( + (IMediaControllerCallback) callback.mCallbackObj); + mBinder.asBinder().unlinkToDeath(callback, 0); + callback.mRegistered = false; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterCallback. " + e); + } + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + if (event == null) { + throw new IllegalArgumentException("event may not be null."); + } + try { + mBinder.sendMediaButton(event); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in dispatchMediaButtonEvent. " + e); + } + return false; + } + + @Override + public TransportControls getTransportControls() { + if (mTransportControls == null) { + mTransportControls = new TransportControlsBase(mBinder); + } + + return mTransportControls; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + try { + return mBinder.getPlaybackState(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackState. " + e); + } + return null; + } + + @Override + public MediaMetadataCompat getMetadata() { + try { + return mBinder.getMetadata(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getMetadata. " + e); + } + return null; + } + + @Override + public List getQueue() { + try { + return mBinder.getQueue(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueue. " + e); + } + return null; + } + + @Override + public CharSequence getQueueTitle() { + try { + return mBinder.getQueueTitle(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueueTitle. " + e); + } + return null; + } + + @Override + public Bundle getExtras() { + try { + return mBinder.getExtras(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getExtras. " + e); + } + return null; + } + + @Override + public int getRatingType() { + try { + return mBinder.getRatingType(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRatingType. " + e); + } + return 0; + } + + @Override + public long getFlags() { + try { + return mBinder.getFlags(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getFlags. " + e); + } + return 0; + } + + @Override + public PlaybackInfo getPlaybackInfo() { + try { + ParcelableVolumeInfo info = mBinder.getVolumeAttributes(); + PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream, + info.controlType, info.maxVolume, info.currentVolume); + return pi; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackInfo. " + e); + } + return null; + } + + @Override + public PendingIntent getSessionActivity() { + try { + return mBinder.getLaunchPendingIntent(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getSessionActivity. " + e); + } + return null; + } + + @Override + public void setVolumeTo(int value, int flags) { + try { + mBinder.setVolumeTo(value, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setVolumeTo. " + e); + } + } + + @Override + public void adjustVolume(int direction, int flags) { + try { + mBinder.adjustVolume(direction, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustVolume. " + e); + } + } + + @Override + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + try { + mBinder.sendCommand(command, params, + new MediaSessionCompat.ResultReceiverWrapper(cb)); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCommand. " + e); + } + } + + @Override + public String getPackageName() { + try { + return mBinder.getPackageName(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPackageName. " + e); + } + return null; + } + + @Override + public Object getMediaController() { + return null; + } + } + + static class TransportControlsBase extends TransportControls { + private IMediaSession mBinder; + + public TransportControlsBase(IMediaSession binder) { + mBinder = binder; + } + + @Override + public void play() { + try { + mBinder.play(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in play. " + e); + } + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) { + try { + mBinder.playFromMediaId(mediaId, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromMediaId. " + e); + } + } + + @Override + public void playFromSearch(String query, Bundle extras) { + try { + mBinder.playFromSearch(query, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromSearch. " + e); + } + } + + @Override + public void skipToQueueItem(long id) { + try { + mBinder.skipToQueueItem(id); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToQueueItem. " + e); + } + } + + @Override + public void pause() { + try { + mBinder.pause(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in pause. " + e); + } + } + + @Override + public void stop() { + try { + mBinder.stop(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in stop. " + e); + } + } + + @Override + public void seekTo(long pos) { + try { + mBinder.seekTo(pos); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in seekTo. " + e); + } + } + + @Override + public void fastForward() { + try { + mBinder.fastForward(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in fastForward. " + e); + } + } + + @Override + public void skipToNext() { + try { + mBinder.next(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToNext. " + e); + } + } + + @Override + public void rewind() { + try { + mBinder.rewind(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in rewind. " + e); + } + } + + @Override + public void skipToPrevious() { + try { + mBinder.previous(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToPrevious. " + e); + } + } + + @Override + public void setRating(RatingCompat rating) { + try { + mBinder.rate(rating); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRating. " + e); + } + } + + @Override + public void sendCustomAction(CustomAction customAction, Bundle args) { + sendCustomAction(customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, Bundle args) { + try { + mBinder.sendCustomAction(action, args); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCustomAction. " + e); + } + } + } + + static class MediaControllerImplApi21 implements MediaControllerImpl { + private final Object mControllerObj; + + public MediaControllerImplApi21(Context context, MediaSessionCompat session) { + mControllerObj = MediaControllerCompatApi21.fromToken(context, + session.getSessionToken().getToken()); + } + + public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + mControllerObj = MediaControllerCompatApi21.fromToken(context, + sessionToken.getToken()); + if (mControllerObj == null) throw new RemoteException(); + } + + @Override + public void registerCallback(Callback callback, Handler handler) { + MediaControllerCompatApi21.registerCallback(mControllerObj, callback.mCallbackObj, handler); + } + + @Override + public void unregisterCallback(Callback callback) { + MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj); + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event); + } + + @Override + public TransportControls getTransportControls() { + Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj); + return controlsObj != null ? new TransportControlsApi21(controlsObj) : null; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj); + return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null; + } + + @Override + public MediaMetadataCompat getMetadata() { + Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj); + return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null; + } + + @Override + public List getQueue() { + List queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj); + if (queueObjs == null) { + return null; + } + List queue = + new ArrayList(); + for (Object item : queueObjs) { + queue.add(MediaSessionCompat.QueueItem.obtain(item)); + } + return queue; + } + + @Override + public CharSequence getQueueTitle() { + return MediaControllerCompatApi21.getQueueTitle(mControllerObj); + } + + @Override + public Bundle getExtras() { + return MediaControllerCompatApi21.getExtras(mControllerObj); + } + + @Override + public int getRatingType() { + return MediaControllerCompatApi21.getRatingType(mControllerObj); + } + + @Override + public long getFlags() { + return MediaControllerCompatApi21.getFlags(mControllerObj); + } + + @Override + public PlaybackInfo getPlaybackInfo() { + Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj); + return volumeInfoObj != null ? new PlaybackInfo( + MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null; + } + + @Override + public PendingIntent getSessionActivity() { + return MediaControllerCompatApi21.getSessionActivity(mControllerObj); + } + + @Override + public void setVolumeTo(int value, int flags) { + MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags); + } + + @Override + public void adjustVolume(int direction, int flags) { + MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags); + } + + @Override + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb); + } + + @Override + public String getPackageName() { + return MediaControllerCompatApi21.getPackageName(mControllerObj); + } + + @Override + public Object getMediaController() { + return mControllerObj; + } + } + + static class TransportControlsApi21 extends TransportControls { + private final Object mControlsObj; + + public TransportControlsApi21(Object controlsObj) { + mControlsObj = controlsObj; + } + + @Override + public void play() { + MediaControllerCompatApi21.TransportControls.play(mControlsObj); + } + + @Override + public void pause() { + MediaControllerCompatApi21.TransportControls.pause(mControlsObj); + } + + @Override + public void stop() { + MediaControllerCompatApi21.TransportControls.stop(mControlsObj); + } + + @Override + public void seekTo(long pos) { + MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos); + } + + @Override + public void fastForward() { + MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj); + } + + @Override + public void rewind() { + MediaControllerCompatApi21.TransportControls.rewind(mControlsObj); + } + + @Override + public void skipToNext() { + MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj); + } + + @Override + public void skipToPrevious() { + MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj); + } + + @Override + public void setRating(RatingCompat rating) { + MediaControllerCompatApi21.TransportControls.setRating(mControlsObj, + rating != null ? rating.getRating() : null); + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) { + MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId, + extras); + } + + @Override + public void playFromSearch(String query, Bundle extras) { + MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query, + extras); + } + + @Override + public void skipToQueueItem(long id) { + MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id); + } + + @Override + public void sendCustomAction(CustomAction customAction, Bundle args) { + MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, + customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, Bundle args) { + MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action, + args); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java new file mode 100644 index 00000000000..6acf4255036 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java @@ -0,0 +1,281 @@ +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +class MediaControllerCompatApi21 { + public static Object fromToken(Context context, Object sessionToken) { + return new MediaController(context, (MediaSession.Token) sessionToken); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static void registerCallback(Object controllerObj, Object callbackObj, Handler handler) { + ((MediaController) controllerObj).registerCallback( + (MediaController.Callback)callbackObj, handler); + } + + public static void unregisterCallback(Object controllerObj, Object callbackObj) { + ((MediaController) controllerObj) + .unregisterCallback((MediaController.Callback) callbackObj); + } + + public static Object getTransportControls(Object controllerObj) { + return ((MediaController)controllerObj).getTransportControls(); + } + + public static Object getPlaybackState(Object controllerObj) { + return ((MediaController)controllerObj).getPlaybackState(); + } + + public static Object getMetadata(Object controllerObj) { + return ((MediaController)controllerObj).getMetadata(); + } + + public static List getQueue(Object controllerObj) { + List queue = ((MediaController) controllerObj).getQueue(); + if (queue == null) { + return null; + } + List queueObjs = new ArrayList(queue); + return queueObjs; + } + + public static CharSequence getQueueTitle(Object controllerObj) { + return ((MediaController) controllerObj).getQueueTitle(); + } + + public static Bundle getExtras(Object controllerObj) { + return ((MediaController) controllerObj).getExtras(); + } + + public static int getRatingType(Object controllerObj) { + return ((MediaController) controllerObj).getRatingType(); + } + + public static long getFlags(Object controllerObj) { + return ((MediaController) controllerObj).getFlags(); + } + + public static Object getPlaybackInfo(Object controllerObj) { + return ((MediaController) controllerObj).getPlaybackInfo(); + } + + public static PendingIntent getSessionActivity(Object controllerObj) { + return ((MediaController) controllerObj).getSessionActivity(); + } + + public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) { + return ((MediaController) controllerObj).dispatchMediaButtonEvent(event); + } + + public static void setVolumeTo(Object controllerObj, int value, int flags) { + ((MediaController) controllerObj).setVolumeTo(value, flags); + } + + public static void adjustVolume(Object controllerObj, int direction, int flags) { + ((MediaController) controllerObj).adjustVolume(direction, flags); + } + + public static void sendCommand(Object controllerObj, + String command, Bundle params, ResultReceiver cb) { + ((MediaController) controllerObj).sendCommand(command, params, cb); + } + + public static String getPackageName(Object controllerObj) { + return ((MediaController) controllerObj).getPackageName(); + } + + public static class TransportControls { + public static void play(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).play(); + } + + public static void pause(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).pause(); + } + + public static void stop(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).stop(); + } + + public static void seekTo(Object controlsObj, long pos) { + ((MediaController.TransportControls)controlsObj).seekTo(pos); + } + + public static void fastForward(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).fastForward(); + } + + public static void rewind(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).rewind(); + } + + public static void skipToNext(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToNext(); + } + + public static void skipToPrevious(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToPrevious(); + } + + public static void setRating(Object controlsObj, Object ratingObj) { + ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj); + } + + public static void playFromMediaId(Object controlsObj, String mediaId, Bundle extras) { + ((MediaController.TransportControls) controlsObj).playFromMediaId(mediaId, extras); + } + + public static void playFromSearch(Object controlsObj, String query, Bundle extras) { + ((MediaController.TransportControls) controlsObj).playFromSearch(query, extras); + } + + public static void skipToQueueItem(Object controlsObj, long id) { + ((MediaController.TransportControls) controlsObj).skipToQueueItem(id); + } + + public static void sendCustomAction(Object controlsObj, String action, Bundle args) { + ((MediaController.TransportControls) controlsObj).sendCustomAction(action, args); + } + } + + public static class PlaybackInfo { + public static int getPlaybackType(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getPlaybackType(); + } + + public static AudioAttributes getAudioAttributes(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo) volumeInfoObj).getAudioAttributes(); + } + + public static int getLegacyAudioStream(Object volumeInfoObj) { + AudioAttributes attrs = getAudioAttributes(volumeInfoObj); + return toLegacyStreamType(attrs); + } + + public static int getVolumeControl(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getVolumeControl(); + } + + public static int getMaxVolume(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getMaxVolume(); + } + + public static int getCurrentVolume(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getCurrentVolume(); + } + + // This is copied from AudioAttributes.toLegacyStreamType. TODO This + // either needs to be kept in sync with that one or toLegacyStreamType + // needs to be made public so it can be used by the support lib. + private static final int FLAG_SCO = 0x1 << 2; + private static final int STREAM_BLUETOOTH_SCO = 6; + private static final int STREAM_SYSTEM_ENFORCED = 7; + private static int toLegacyStreamType(AudioAttributes aa) { + // flags to stream type mapping + if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) { + return STREAM_SYSTEM_ENFORCED; + } + if ((aa.getFlags() & FLAG_SCO) == FLAG_SCO) { + return STREAM_BLUETOOTH_SCO; + } + + // usage to stream type mapping + switch (aa.getUsage()) { + case AudioAttributes.USAGE_MEDIA: + case AudioAttributes.USAGE_GAME: + case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY: + case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return AudioManager.STREAM_MUSIC; + case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION: + return AudioManager.STREAM_SYSTEM; + case AudioAttributes.USAGE_VOICE_COMMUNICATION: + return AudioManager.STREAM_VOICE_CALL; + case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return AudioManager.STREAM_DTMF; + case AudioAttributes.USAGE_ALARM: + return AudioManager.STREAM_ALARM; + case AudioAttributes.USAGE_NOTIFICATION_RINGTONE: + return AudioManager.STREAM_RING; + case AudioAttributes.USAGE_NOTIFICATION: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case AudioAttributes.USAGE_NOTIFICATION_EVENT: + return AudioManager.STREAM_NOTIFICATION; + case AudioAttributes.USAGE_UNKNOWN: + default: + return AudioManager.STREAM_MUSIC; + } + } + } + + public static interface Callback { + public void onSessionDestroyed(); + public void onSessionEvent(String event, Bundle extras); + public void onPlaybackStateChanged(Object stateObj); + public void onMetadataChanged(Object metadataObj); + } + + static class CallbackProxy extends MediaController.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onSessionDestroyed() { + mCallback.onSessionDestroyed(); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + mCallback.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + mCallback.onPlaybackStateChanged(state); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mCallback.onMetadataChanged(metadata); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl new file mode 100644 index 00000000000..d0c2f6fce2c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl @@ -0,0 +1,20 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable MediaSessionCompat.Token; +parcelable MediaSessionCompat.QueueItem; +parcelable MediaSessionCompat.ResultReceiverWrapper; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java new file mode 100644 index 00000000000..b8beb78bd55 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java @@ -0,0 +1,1924 @@ + +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.SystemClock; +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.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Allows interaction with media controllers, volume keys, media buttons, and + * transport controls. + *

+ * A MediaSession should be created when an app wants to publish media playback + * information or handle media keys. In general an app only needs one session + * for all playback, though multiple sessions can be created to provide finer + * grain controls of media. + *

+ * Once a session is created the owner of the session may pass its + * {@link #getSessionToken() session token} to other processes to allow them to + * create a {@link MediaControllerCompat} to interact with the session. + *

+ * To receive commands, media keys, and other events a {@link Callback} must be + * set with {@link #setCallback(Callback)}. + *

+ * When an app is finished performing playback it must call {@link #release()} + * to clean up the session and notify any controllers. + *

+ * MediaSessionCompat objects are not thread safe and all calls should be made + * from the same thread. + *

+ * This is a helper for accessing features in + * {@link android.media.session.MediaSession} introduced after API level 4 in a + * backwards compatible fashion. + */ +public class MediaSessionCompat { + private final MediaSessionImpl mImpl; + private final MediaControllerCompat mController; + private final ArrayList + mActiveListeners = new ArrayList(); + + /** + * Set this flag on the session to indicate that it can handle media button + * events. + */ + public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; + + /** + * Set this flag on the session to indicate that it handles transport + * control commands through its {@link Callback}. + */ + public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; + + /** + * Creates a new session. + * + * @param context The context. + * @param tag A short name for debugging purposes. + * @param mediaButtonEventReceiver The component name for your receiver. + * This must be non-null to support platform versions earlier + * than {@link android.os.Build.VERSION_CODES#LOLLIPOP}. + * @param mbrIntent The PendingIntent for your receiver component that + * handles media button events. This is optional and will be used + * on {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and + * later instead of the component name. + */ + public MediaSessionCompat(Context context, String tag, ComponentName mediaButtonEventReceiver, + PendingIntent mbrIntent) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("tag must not be null or empty"); + } + + if (mediaButtonEventReceiver != null && mbrIntent == null) { + // construct a PendingIntent for the media button + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + // the associated intent will be handled by the component being registered + mediaButtonIntent.setComponent(mediaButtonEventReceiver); + mbrIntent = PendingIntent.getBroadcast(context, + 0/* requestCode, ignored */, mediaButtonIntent, 0/* flags */); + } + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaSessionImplApi21(context, tag); + mImpl.setMediaButtonReceiver(mbrIntent); + } else { + mImpl = new MediaSessionImplBase(context, tag, mediaButtonEventReceiver, mbrIntent); + } + mController = new MediaControllerCompat(context, this); + } + + private MediaSessionCompat(Context context, MediaSessionImpl impl) { + mImpl = impl; + mController = new MediaControllerCompat(context, this); + } + + /** + * Add a callback to receive updates on for the MediaSession. This includes + * media button and volume events. The caller's thread will be used to post + * events. + * + * @param callback The callback object + */ + public void setCallback(Callback callback) { + setCallback(callback, null); + } + + /** + * Set the callback to receive updates for the MediaSession. This includes + * media button and volume events. Set the callback to null to stop + * receiving events. + * + * @param callback The callback to receive updates on. + * @param handler The handler that events should be posted on. + */ + public void setCallback(Callback callback, Handler handler) { + mImpl.setCallback(callback, handler != null ? handler : new Handler()); + } + + /** + * Set an intent for launching UI for this Session. This can be used as a + * quick link to an ongoing media screen. The intent should be for an + * activity that may be started using + * {@link Activity#startActivity(Intent)}. + * + * @param pi The intent to launch to show UI for this Session. + */ + public void setSessionActivity(PendingIntent pi) { + mImpl.setSessionActivity(pi); + } + + /** + * Set a pending intent for your media button receiver to allow restarting + * playback after the session has been stopped. If your app is started in + * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via + * the pending intent. + *

+ * This method will only work on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. Earlier + * platform versions must include the media button receiver in the + * constructor. + * + * @param mbr The {@link PendingIntent} to send the media button event to. + */ + public void setMediaButtonReceiver(PendingIntent mbr) { + mImpl.setMediaButtonReceiver(mbr); + } + + /** + * Set any flags for the session. + * + * @param flags The flags to set for this session. + */ + public void setFlags(int flags) { + mImpl.setFlags(flags); + } + + /** + * Set the stream this session is playing on. This will affect the system's + * volume handling for this session. If {@link #setPlaybackToRemote} was + * previously called it will stop receiving volume commands and the system + * will begin sending volume changes to the appropriate stream. + *

+ * By default sessions are on {@link AudioManager#STREAM_MUSIC}. + * + * @param stream The {@link AudioManager} stream this session is playing on. + */ + public void setPlaybackToLocal(int stream) { + mImpl.setPlaybackToLocal(stream); + } + + /** + * Configure this session to use remote volume handling. This must be called + * to receive volume button events, otherwise the system will adjust the + * current stream volume for this session. If {@link #setPlaybackToLocal} + * was previously called that stream will stop receiving volume changes for + * this session. + *

+ * On platforms earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP} + * this will only allow an app to handle volume commands sent directly to + * the session by a {@link MediaControllerCompat}. System routing of volume + * keys will not use the volume provider. + * + * @param volumeProvider The provider that will handle volume changes. May + * not be null. + */ + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null!"); + } + mImpl.setPlaybackToRemote(volumeProvider); + } + + /** + * Set if this session is currently active and ready to receive commands. If + * set to false your session's controller may not be discoverable. You must + * set the session to active before it can start receiving media button + * events or transport commands. + *

+ * On platforms earlier than + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, + * {@link #setMediaButtonReceiver(PendingIntent)} must be called before + * setting this to true. + * + * @param active Whether this session is active or not. + */ + public void setActive(boolean active) { + mImpl.setActive(active); + for (OnActiveChangeListener listener : mActiveListeners) { + listener.onActiveChanged(); + } + } + + /** + * Get the current active state of this session. + * + * @return True if the session is active, false otherwise. + */ + public boolean isActive() { + return mImpl.isActive(); + } + + /** + * Send a proprietary event to all MediaControllers listening to this + * Session. It's up to the Controller/Session owner to determine the meaning + * of any events. + * + * @param event The name of the event to send + * @param extras Any extras included with the event + */ + public void sendSessionEvent(String event, Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } + mImpl.sendSessionEvent(event, extras); + } + + /** + * This must be called when an app has finished performing playback. If + * playback is expected to start again shortly the session can be left open, + * but it must be released if your activity or service is being destroyed. + */ + public void release() { + mImpl.release(); + } + + /** + * Retrieve a token object that can be used by apps to create a + * {@link MediaControllerCompat} for interacting with this session. The + * owner of the session is responsible for deciding how to distribute these + * tokens. + *

+ * On platform versions before + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} this token may only be + * used within your app as there is no way to guarantee other apps are using + * the same version of the support library. + * + * @return A token that can be used to create a media controller for this + * session. + */ + public Token getSessionToken() { + return mImpl.getSessionToken(); + } + + /** + * Get a controller for this session. This is a convenience method to avoid + * having to cache your own controller in process. + * + * @return A controller for this session. + */ + public MediaControllerCompat getController() { + return mController; + } + + /** + * Update the current playback state. + * + * @param state The current state of playback + */ + public void setPlaybackState(PlaybackStateCompat state) { + mImpl.setPlaybackState(state); + } + + /** + * Update the current metadata. New metadata can be created using + * {@link android.media.MediaMetadata.Builder}. + * + * @param metadata The new metadata + */ + public void setMetadata(MediaMetadataCompat metadata) { + mImpl.setMetadata(metadata); + } + + /** + * Update the list of items in the play queue. It is an ordered list and + * should contain the current item, and previous or upcoming items if they + * exist. Specify null if there is no current play queue. + *

+ * The queue should be of reasonable size. If the play queue is unbounded + * within your app, it is better to send a reasonable amount in a sliding + * window instead. + * + * @param queue A list of items in the play queue. + */ + public void setQueue(List queue) { + mImpl.setQueue(queue); + } + + /** + * Set the title of the play queue. The UI should display this title along + * with the play queue itself. e.g. "Play Queue", "Now Playing", or an album + * name. + * + * @param title The title of the play queue. + */ + public void setQueueTitle(CharSequence title) { + mImpl.setQueueTitle(title); + } + + /** + * Set the style of rating used by this session. Apps trying to set the + * rating should use this style. Must be one of the following: + *

    + *
  • {@link RatingCompat#RATING_NONE}
  • + *
  • {@link RatingCompat#RATING_3_STARS}
  • + *
  • {@link RatingCompat#RATING_4_STARS}
  • + *
  • {@link RatingCompat#RATING_5_STARS}
  • + *
  • {@link RatingCompat#RATING_HEART}
  • + *
  • {@link RatingCompat#RATING_PERCENTAGE}
  • + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN}
  • + *
+ */ + public void setRatingType(int type) { + mImpl.setRatingType(type); + } + + /** + * 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 + * fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. + * + * @param extras The extras associated with the session. + */ + public void setExtras(Bundle extras) { + mImpl.setExtras(extras); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession} + * object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaSession} object, + * or null if none. + */ + public Object getMediaSession() { + return mImpl.getMediaSession(); + } + + /** + * Gets the underlying framework {@link android.media.RemoteControlClient} + * object. + *

+ * This method is only supported on APIs 14-20. On API 21+ + * {@link #getMediaSession()} should be used instead. + * + * @return The underlying {@link android.media.RemoteControlClient} object, + * or null if none. + */ + public Object getRemoteControlClient() { + return mImpl.getRemoteControlClient(); + } + + /** + * Adds a listener to be notified when the active status of this session + * changes. This is primarily used by the support library and should not be + * needed by apps. + * + * @param listener The listener to add. + */ + public void addOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.add(listener); + } + + /** + * Stops the listener from being notified when the active status of this + * session changes. + * + * @param listener The listener to remove. + */ + public void removeOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.remove(listener); + } + + /** + * Obtain a compat wrapper for an existing MediaSession. + * + * @param mediaSession The {@link android.media.session.MediaSession} to + * wrap. + * @return A compat wrapper for the provided session. + */ + public static MediaSessionCompat obtain(Context context, Object mediaSession) { + return new MediaSessionCompat(context, new MediaSessionImplApi21(mediaSession)); + } + + /** + * Receives transport controls, media buttons, and commands from controllers + * and the system. The callback may be set using {@link #setCallback}. + */ + public abstract static class Callback { + final Object mCallbackObj; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = null; + } + } + + /** + * Called when a controller has sent a custom command to this session. + * The owner of the session may handle custom commands but is not + * required to. + * + * @param command The command name. + * @param extras Optional parameters for the command, may be null. + * @param cb A result receiver to which a result may be sent by the command, may be null. + */ + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + } + + /** + * Override to handle media button events. + * + * @param mediaButtonEvent The media button event intent. + * @return True if the event was handled, false otherwise. + */ + public boolean onMediaButtonEvent(Intent mediaButtonEvent) { + return false; + } + + /** + * Override to handle requests to begin playback. + */ + public void onPlay() { + } + + /** + * Override to handle requests to play a specific mediaId that was + * provided by your app. + */ + public void onPlayFromMediaId(String mediaId, Bundle extras) { + } + + /** + * Override to handle requests to begin playback from a search query. An + * empty query indicates that the app may play any music. The + * implementation should attempt to make a smart choice about what to + * play. + */ + public void onPlayFromSearch(String query, Bundle extras) { + } + + /** + * Override to handle requests to play an item with a given id from the + * play queue. + */ + public void onSkipToQueueItem(long id) { + } + + /** + * Override to handle requests to pause playback. + */ + public void onPause() { + } + + /** + * Override to handle requests to skip to the next media item. + */ + public void onSkipToNext() { + } + + /** + * Override to handle requests to skip to the previous media item. + */ + public void onSkipToPrevious() { + } + + /** + * Override to handle requests to fast forward. + */ + public void onFastForward() { + } + + /** + * Override to handle requests to rewind. + */ + public void onRewind() { + } + + /** + * Override to handle requests to stop playback. + */ + public void onStop() { + } + + /** + * Override to handle requests to seek to a specific position in ms. + * + * @param pos New position to move to, in milliseconds. + */ + public void onSeekTo(long pos) { + } + + /** + * Override to handle the item being rated. + * + * @param rating + */ + public void onSetRating(RatingCompat rating) { + } + + /** + * Called when a {@link MediaControllerCompat} wants a + * {@link PlaybackStateCompat.CustomAction} to be performed. + * + * @param action The action that was originally sent in the + * {@link PlaybackStateCompat.CustomAction}. + * @param extras Optional extras specified by the + * {@link MediaControllerCompat}. + */ + public void onCustomAction(String action, Bundle extras) { + } + + private class StubApi21 implements MediaSessionCompatApi21.Callback { + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + Callback.this.onCommand(command, extras, cb); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return Callback.this.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onPlay() { + Callback.this.onPlay(); + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + Callback.this.onPlayFromMediaId(mediaId, extras); + } + + @Override + public void onPlayFromSearch(String search, Bundle extras) { + Callback.this.onPlayFromSearch(search, extras); + } + + @Override + public void onSkipToQueueItem(long id) { + Callback.this.onSkipToQueueItem(id); + } + + @Override + public void onPause() { + Callback.this.onPause(); + } + + @Override + public void onSkipToNext() { + Callback.this.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + Callback.this.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + Callback.this.onFastForward(); + } + + @Override + public void onRewind() { + Callback.this.onRewind(); + } + + @Override + public void onStop() { + Callback.this.onStop(); + } + + @Override + public void onSeekTo(long pos) { + Callback.this.onSeekTo(pos); + } + + @Override + public void onSetRating(Object ratingObj) { + Callback.this.onSetRating(RatingCompat.fromRating(ratingObj)); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + Callback.this.onCustomAction(action, extras); + } + } + } + + /** + * Represents an ongoing session. This may be passed to apps by the session + * owner to allow them to create a {@link MediaControllerCompat} to communicate with + * the session. + */ + public static final class Token implements Parcelable { + private final Object mInner; + + Token(Object inner) { + mInner = inner; + } + + /** + * Creates a compat Token from a framework + * {@link android.media.session.MediaSession.Token} object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @param token The framework token object. + * @return A compat Token for use with {@link MediaControllerCompat}. + */ + public static Token fromToken(Object token) { + if (token == null || android.os.Build.VERSION.SDK_INT < 21) { + return null; + } + return new Token(MediaSessionCompatApi21.verifyToken(token)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (android.os.Build.VERSION.SDK_INT >= 21) { + dest.writeParcelable((Parcelable) mInner, flags); + } else { + dest.writeStrongBinder((IBinder) mInner); + } + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaSession.Token} object, + * or null if none. + */ + public Object getToken() { + return mInner; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public Token createFromParcel(Parcel in) { + Object inner; + if (android.os.Build.VERSION.SDK_INT >= 21) { + inner = in.readParcelable(null); + } else { + inner = in.readStrongBinder(); + } + return new Token(inner); + } + + @Override + public Token[] newArray(int size) { + return new Token[size]; + } + }; + } + + /** + * A single item that is part of the play queue. It contains a description + * of the item and its id in the queue. + */ + public static final class QueueItem implements Parcelable { + /** + * This id is reserved. No items can be explicitly asigned this id. + */ + public static final int UNKNOWN_ID = -1; + + private final MediaDescriptionCompat mDescription; + private final long mId; + + private Object mItem; + + /** + * Create a new {@link MediaSessionCompat.QueueItem}. + * + * @param description The {@link MediaDescriptionCompat} for this item. + * @param id An identifier for this item. It must be unique within the + * play queue and cannot be {@link #UNKNOWN_ID}. + */ + public QueueItem(MediaDescriptionCompat description, long id) { + this(null, description, id); + } + + private QueueItem(Object queueItem, MediaDescriptionCompat description, long id) { + if (description == null) { + throw new IllegalArgumentException("Description cannot be null."); + } + if (id == UNKNOWN_ID) { + throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); + } + mDescription = description; + mId = id; + mItem = queueItem; + } + + private QueueItem(Parcel in) { + mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); + mId = in.readLong(); + } + + /** + * Get the description for this item. + */ + public MediaDescriptionCompat getDescription() { + return mDescription; + } + + /** + * Get the queue id for this item. + */ + public long getQueueId() { + return mId; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mDescription.writeToParcel(dest, flags); + dest.writeLong(mId); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Get the underlying + * {@link android.media.session.MediaSession.QueueItem}. + *

+ * On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null + * is returned. + * + * @return The underlying + * {@link android.media.session.MediaSession.QueueItem} or null. + */ + public Object getQueueItem() { + if (mItem != null || android.os.Build.VERSION.SDK_INT < 21) { + return mItem; + } + mItem = MediaSessionCompatApi21.QueueItem.createItem(mDescription.getMediaDescription(), + mId); + return mItem; + } + + /** + * Obtain a compat wrapper for an existing QueueItem. + * + * @param queueItem The {@link android.media.session.MediaSession.QueueItem} to + * wrap. + * @return A compat wrapper for the provided item. + */ + public static QueueItem obtain(Object queueItem) { + Object descriptionObj = MediaSessionCompatApi21.QueueItem.getDescription(queueItem); + MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription( + descriptionObj); + long id = MediaSessionCompatApi21.QueueItem.getQueueId(queueItem); + return new QueueItem(queueItem, description, id); + } + + public static final Creator + CREATOR = new Creator() { + + @Override + public MediaSessionCompat.QueueItem createFromParcel(Parcel p) { + return new MediaSessionCompat.QueueItem(p); + } + + @Override + public MediaSessionCompat.QueueItem[] newArray(int size) { + return new MediaSessionCompat.QueueItem[size]; + } + }; + + @Override + public String toString() { + return "MediaSession.QueueItem {" + + "Description=" + mDescription + + ", Id=" + mId + " }"; + } + } + + /** + * This is a wrapper for {@link ResultReceiver} for sending over aidl + * interfaces. The framework version was not exposed to aidls until + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. + */ + static final class ResultReceiverWrapper implements Parcelable { + private ResultReceiver mResultReceiver; + + public ResultReceiverWrapper(ResultReceiver resultReceiver) { + mResultReceiver = resultReceiver; + } + + ResultReceiverWrapper(Parcel in) { + mResultReceiver = ResultReceiver.CREATOR.createFromParcel(in); + } + + public static final Creator + CREATOR = new Creator() { + @Override + public ResultReceiverWrapper createFromParcel(Parcel p) { + return new ResultReceiverWrapper(p); + } + + @Override + public ResultReceiverWrapper[] newArray(int size) { + return new ResultReceiverWrapper[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mResultReceiver.writeToParcel(dest, flags); + } + } + + public interface OnActiveChangeListener { + void onActiveChanged(); + } + + interface MediaSessionImpl { + void setCallback(Callback callback, Handler handler); + void setFlags(int flags); + void setPlaybackToLocal(int stream); + void setPlaybackToRemote(VolumeProviderCompat volumeProvider); + void setActive(boolean active); + boolean isActive(); + void sendSessionEvent(String event, Bundle extras); + void release(); + Token getSessionToken(); + void setPlaybackState(PlaybackStateCompat state); + void setMetadata(MediaMetadataCompat metadata); + + void setSessionActivity(PendingIntent pi); + + void setMediaButtonReceiver(PendingIntent mbr); + void setQueue(List queue); + void setQueueTitle(CharSequence title); + + void setRatingType(int type); + void setExtras(Bundle extras); + + Object getMediaSession(); + + Object getRemoteControlClient(); + } + + static class MediaSessionImplBase implements MediaSessionImpl { + private final Context mContext; + private final ComponentName mComponentName; + private final PendingIntent mMediaButtonEventReceiver; + private final Object mRccObj; + private final MediaSessionStub mStub; + private final Token mToken; + private final MessageHandler mHandler; + private final String mPackageName; + private final String mTag; + private final AudioManager mAudioManager; + + private final Object mLock = new Object(); + private final RemoteCallbackList mControllerCallbacks + = new RemoteCallbackList(); + + private boolean mDestroyed = false; + private boolean mIsActive = false; + private boolean mIsRccRegistered = false; + private boolean mIsMbrRegistered = false; + private Callback mCallback; + + private int mFlags; + + private MediaMetadataCompat mMetadata; + private PlaybackStateCompat mState; + private PendingIntent mSessionActivity; + private List mQueue; + private CharSequence mQueueTitle; + private int mRatingType; + private Bundle mExtras; + + private int mVolumeType; + private int mLocalStream; + private VolumeProviderCompat mVolumeProvider; + + private VolumeProviderCompat.Callback mVolumeCallback + = new VolumeProviderCompat.Callback() { + @Override + public void onVolumeChanged(VolumeProviderCompat volumeProvider) { + if (mVolumeProvider != volumeProvider) { + return; + } + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + volumeProvider.getVolumeControl(), volumeProvider.getMaxVolume(), + volumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + } + }; + + public MediaSessionImplBase(Context context, String tag, ComponentName mbrComponent, + PendingIntent mbr) { + if (mbrComponent == null) { + throw new IllegalArgumentException( + "MediaButtonReceiver component may not be null."); + } + mContext = context; + mPackageName = context.getPackageName(); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mTag = tag; + mComponentName = mbrComponent; + mMediaButtonEventReceiver = mbr; + mStub = new MediaSessionStub(); + mToken = new Token(mStub); + mHandler = new MessageHandler(Looper.myLooper()); + + mRatingType = RatingCompat.RATING_NONE; + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + mLocalStream = AudioManager.STREAM_MUSIC; + if (android.os.Build.VERSION.SDK_INT >= 14) { + mRccObj = MediaSessionCompatApi14.createRemoteControlClient(mbr); + } else { + mRccObj = null; + } + } + + @Override + public void setCallback(final Callback callback, Handler handler) { + if (callback == mCallback) { + return; + } + if (callback == null || android.os.Build.VERSION.SDK_INT < 18) { + // There's nothing to register on API < 18 since media buttons + // all go through the media button receiver + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj, null); + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj, null); + } + } else { + if (handler == null) { + handler = new Handler(); + } + MediaSessionCompatApi14.Callback cb14 = new MediaSessionCompatApi14.Callback() { + @Override + public void onStop() { + callback.onStop(); + } + + @Override + public void onSkipToPrevious() { + callback.onSkipToPrevious(); + } + + @Override + public void onSkipToNext() { + callback.onSkipToNext(); + } + + @Override + public void onSetRating(Object ratingObj) { + callback.onSetRating(RatingCompat.fromRating(ratingObj)); + } + + @Override + public void onSeekTo(long pos) { + callback.onSeekTo(pos); + } + + @Override + public void onRewind() { + callback.onRewind(); + } + + @Override + public void onPlay() { + callback.onPlay(); + } + + @Override + public void onPause() { + callback.onPause(); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return callback.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onFastForward() { + callback.onFastForward(); + } + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + callback.onCommand(command, extras, cb); + } + }; + if (android.os.Build.VERSION.SDK_INT >= 18) { + Object onPositionUpdateObj = MediaSessionCompatApi18 + .createPlaybackPositionUpdateListener(cb14); + MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj, + onPositionUpdateObj); + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + Object onMetadataUpdateObj = MediaSessionCompatApi19 + .createMetadataUpdateListener(cb14); + MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj, + onMetadataUpdateObj); + } + } + mCallback = callback; + } + + @Override + public void setFlags(int flags) { + synchronized (mLock) { + mFlags = flags; + } + update(); + } + + @Override + public void setPlaybackToLocal(int stream) { + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, + mAudioManager.getStreamMaxVolume(mLocalStream), + mAudioManager.getStreamVolume(mLocalStream)); + sendVolumeInfoChanged(info); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null"); + } + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + mVolumeProvider = volumeProvider; + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + mVolumeProvider.getVolumeControl(), mVolumeProvider.getMaxVolume(), + mVolumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + + volumeProvider.setCallback(mVolumeCallback); + } + + @Override + public void setActive(boolean active) { + if (active == mIsActive) { + return; + } + mIsActive = active; + if (update()) { + setMetadata(mMetadata); + setPlaybackState(mState); + } + } + + @Override + public boolean isActive() { + return mIsActive; + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + sendEvent(event, extras); + } + + @Override + public void release() { + mIsActive = false; + mDestroyed = true; + update(); + sendSessionDestroyed(); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + synchronized (mLock) { + mState = state; + } + sendState(state); + if (!mIsActive) { + // Don't set the state until after the RCC is registered + return; + } + if (state == null) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setState(mRccObj, PlaybackStateCompat.STATE_NONE); + MediaSessionCompatApi14.setTransportControlFlags(mRccObj, 0); + } + } else { + // Set state + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setState(mRccObj, state.getState(), state.getPosition(), + state.getPlaybackSpeed(), state.getLastPositionUpdateTime()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setState(mRccObj, state.getState()); + } + + // Set transport control flags + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setTransportControlFlags(mRccObj, state.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setTransportControlFlags(mRccObj, state.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setTransportControlFlags(mRccObj, state.getActions()); + } + } + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + synchronized (mLock) { + mMetadata = metadata; + } + sendMetadata(metadata); + if (!mIsActive) { + // Don't set metadata until after the rcc has been registered + return; + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setMetadata(mRccObj, + metadata == null ? null : metadata.getBundle(), + mState == null ? 0 : mState.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setMetadata(mRccObj, + metadata == null ? null : metadata.getBundle()); + } + } + + @Override + public void setSessionActivity(PendingIntent pi) { + synchronized (mLock) { + mSessionActivity = pi; + } + } + + @Override + public void setMediaButtonReceiver(PendingIntent mbr) { + // Do nothing, changing this is not supported before API 21. + } + + @Override + public void setQueue(List queue) { + mQueue = queue; + sendQueue(queue); + } + + @Override + public void setQueueTitle(CharSequence title) { + mQueueTitle = title; + sendQueueTitle(title); + } + + @Override + public Object getMediaSession() { + return null; + } + + @Override + public Object getRemoteControlClient() { + return mRccObj; + } + + @Override + public void setRatingType(int type) { + mRatingType = type; + } + + @Override + public void setExtras(Bundle extras) { + mExtras = extras; + } + + // Registers/unregisters the RCC and MediaButtonEventReceiver as needed. + private boolean update() { + boolean registeredRcc = false; + if (mIsActive) { + // On API 8+ register a MBR if it's supported, unregister it + // if support was removed. + if (android.os.Build.VERSION.SDK_INT >= 8) { + if (!mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) != 0) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.registerMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.registerMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = true; + } else if (mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) == 0) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = false; + } + } + // On API 14+ register a RCC if it's supported, unregister it if + // not. + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (!mIsRccRegistered && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) { + MediaSessionCompatApi14.registerRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = true; + registeredRcc = true; + } else if (mIsRccRegistered + && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) == 0) { + MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = false; + } + } + } else { + // When inactive remove any registered components. + if (mIsMbrRegistered) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = false; + } + if (mIsRccRegistered) { + MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = false; + } + } + return registeredRcc; + } + + private void adjustVolume(int direction, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onAdjustVolume(direction); + } + } else { + mAudioManager.adjustStreamVolume(direction, mLocalStream, flags); + } + } + + private void setVolumeTo(int value, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onSetVolumeTo(value); + } + } else { + mAudioManager.setStreamVolume(mLocalStream, value, flags); + } + } + + private PlaybackStateCompat getStateWithUpdatedPosition() { + PlaybackStateCompat state; + long duration = -1; + synchronized (mLock) { + state = mState; + if (mMetadata != null + && mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { + duration = mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); + } + } + + PlaybackStateCompat result = null; + if (state != null) { + if (state.getState() == PlaybackStateCompat.STATE_PLAYING + || state.getState() == PlaybackStateCompat.STATE_FAST_FORWARDING + || state.getState() == PlaybackStateCompat.STATE_REWINDING) { + long updateTime = state.getLastPositionUpdateTime(); + long currentTime = SystemClock.elapsedRealtime(); + if (updateTime > 0) { + long position = (long) (state.getPlaybackSpeed() + * (currentTime - updateTime)) + state.getPosition(); + if (duration >= 0 && position > duration) { + position = duration; + } else if (position < 0) { + position = 0; + } + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder( + state); + builder.setState(state.getState(), position, state.getPlaybackSpeed(), + currentTime); + result = builder.build(); + } + } + } + return result == null ? state : result; + } + + private void sendVolumeInfoChanged(ParcelableVolumeInfo info) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onVolumeInfoChanged(info); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendSessionDestroyed() { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onSessionDestroyed();; + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + mControllerCallbacks.kill(); + } + + private void sendEvent(String event, Bundle extras) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onEvent(event, extras); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendState(PlaybackStateCompat state) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onPlaybackStateChanged(state); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendMetadata(MediaMetadataCompat metadata) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onMetadataChanged(metadata); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendQueue(List queue) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueChanged(queue); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendQueueTitle(CharSequence queueTitle) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueTitleChanged(queueTitle); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + class MediaSessionStub extends IMediaSession.Stub { + @Override + public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { + mHandler.post(MessageHandler.MSG_COMMAND, + new Command(command, args, cb.mResultReceiver)); + } + + @Override + public boolean sendMediaButton(KeyEvent mediaButton) { + boolean handlesMediaButtons = + (mFlags & MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS) != 0; + if (handlesMediaButtons) { + mHandler.post(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); + } + return handlesMediaButtons; + } + + @Override + public void registerCallbackListener(IMediaControllerCallback cb) { + // If this session is already destroyed tell the caller and + // don't add them. + if (mDestroyed) { + try { + cb.onSessionDestroyed(); + } catch (Exception e) { + // ignored + } + return; + } + mControllerCallbacks.register(cb); + } + + @Override + public void unregisterCallbackListener(IMediaControllerCallback cb) { + mControllerCallbacks.unregister(cb); + } + + @Override + public String getPackageName() { + // mPackageName is final so doesn't need synchronize block + return mPackageName; + } + + @Override + public String getTag() { + // mTag is final so doesn't need synchronize block + return mTag; + } + + @Override + public PendingIntent getLaunchPendingIntent() { + synchronized (mLock) { + return mSessionActivity; + } + } + + @Override + public long getFlags() { + synchronized (mLock) { + return mFlags; + } + } + + @Override + public ParcelableVolumeInfo getVolumeAttributes() { + int controlType; + int max; + int current; + int stream; + int volumeType; + synchronized (mLock) { + volumeType = mVolumeType; + stream = mLocalStream; + VolumeProviderCompat vp = mVolumeProvider; + if (volumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + controlType = vp.getVolumeControl(); + max = vp.getMaxVolume(); + current = vp.getCurrentVolume(); + } else { + controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + max = mAudioManager.getStreamMaxVolume(stream); + current = mAudioManager.getStreamVolume(stream); + } + } + return new ParcelableVolumeInfo(volumeType, stream, controlType, max, current); + } + + @Override + public void adjustVolume(int direction, int flags, String packageName) { + MediaSessionImplBase.this.adjustVolume(direction, flags); + } + + @Override + public void setVolumeTo(int value, int flags, String packageName) { + MediaSessionImplBase.this.setVolumeTo(value, flags); + } + + @Override + public void play() throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY); + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); + } + + @Override + public void playFromSearch(String query, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY_SEARCH, query, extras); + } + + @Override + public void skipToQueueItem(long id) { + mHandler.post(MessageHandler.MSG_SKIP_TO_ITEM, id); + } + + @Override + public void pause() throws RemoteException { + mHandler.post(MessageHandler.MSG_PAUSE); + } + + @Override + public void stop() throws RemoteException { + mHandler.post(MessageHandler.MSG_STOP); + } + + @Override + public void next() throws RemoteException { + mHandler.post(MessageHandler.MSG_NEXT); + } + + @Override + public void previous() throws RemoteException { + mHandler.post(MessageHandler.MSG_PREVIOUS); + } + + @Override + public void fastForward() throws RemoteException { + mHandler.post(MessageHandler.MSG_FAST_FORWARD); + } + + @Override + public void rewind() throws RemoteException { + mHandler.post(MessageHandler.MSG_REWIND); + } + + @Override + public void seekTo(long pos) throws RemoteException { + mHandler.post(MessageHandler.MSG_SEEK_TO, pos); + } + + @Override + public void rate(RatingCompat rating) throws RemoteException { + mHandler.post(MessageHandler.MSG_RATE, rating); + } + + @Override + public void sendCustomAction(String action, Bundle args) + throws RemoteException { + mHandler.post(MessageHandler.MSG_CUSTOM_ACTION, action, args); + } + + @Override + public MediaMetadataCompat getMetadata() { + return mMetadata; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + return getStateWithUpdatedPosition(); + } + + @Override + public List getQueue() { + synchronized (mLock) { + return mQueue; + } + } + + @Override + public CharSequence getQueueTitle() { + return mQueueTitle; + } + + @Override + public Bundle getExtras() { + synchronized (mLock) { + return mExtras; + } + } + + @Override + public int getRatingType() { + return mRatingType; + } + + @Override + public boolean isTransportControlEnabled() { + return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0; + } + } + + private static final class Command { + public final String command; + public final Bundle extras; + public final ResultReceiver stub; + + public Command(String command, Bundle extras, ResultReceiver stub) { + this.command = command; + this.extras = extras; + this.stub = stub; + } + } + + private class MessageHandler extends Handler { + + private static final int MSG_PLAY = 1; + private static final int MSG_PLAY_MEDIA_ID = 2; + private static final int MSG_PLAY_SEARCH = 3; + private static final int MSG_SKIP_TO_ITEM = 4; + private static final int MSG_PAUSE = 5; + private static final int MSG_STOP = 6; + private static final int MSG_NEXT = 7; + private static final int MSG_PREVIOUS = 8; + private static final int MSG_FAST_FORWARD = 9; + private static final int MSG_REWIND = 10; + private static final int MSG_SEEK_TO = 11; + private static final int MSG_RATE = 12; + private static final int MSG_CUSTOM_ACTION = 13; + private static final int MSG_MEDIA_BUTTON = 14; + private static final int MSG_COMMAND = 15; + private static final int MSG_ADJUST_VOLUME = 16; + private static final int MSG_SET_VOLUME = 17; + + public MessageHandler(Looper looper) { + super(looper); + } + + public void post(int what, Object obj, Bundle bundle) { + Message msg = obtainMessage(what, obj); + msg.setData(bundle); + msg.sendToTarget(); + } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + + public void post(int what) { + post(what, null); + } + + public void post(int what, Object obj, int arg1) { + obtainMessage(what, arg1, 0, obj).sendToTarget(); + } + + @Override + public void handleMessage(Message msg) { + if (mCallback == null) { + return; + } + switch (msg.what) { + case MSG_PLAY: + mCallback.onPlay(); + break; + case MSG_PLAY_MEDIA_ID: + mCallback.onPlayFromMediaId((String) msg.obj, msg.getData()); + break; + case MSG_PLAY_SEARCH: + mCallback.onPlayFromSearch((String) msg.obj, msg.getData()); + break; + case MSG_SKIP_TO_ITEM: + mCallback.onSkipToQueueItem((Long) msg.obj); + break; + case MSG_PAUSE: + mCallback.onPause(); + break; + case MSG_STOP: + mCallback.onStop(); + break; + case MSG_NEXT: + mCallback.onSkipToNext(); + break; + case MSG_PREVIOUS: + mCallback.onSkipToPrevious(); + break; + case MSG_FAST_FORWARD: + mCallback.onFastForward(); + break; + case MSG_REWIND: + mCallback.onRewind(); + break; + case MSG_SEEK_TO: + mCallback.onSeekTo((Long) msg.obj); + break; + case MSG_RATE: + mCallback.onSetRating((RatingCompat) msg.obj); + break; + case MSG_CUSTOM_ACTION: + mCallback.onCustomAction((String) msg.obj, msg.getData()); + break; + case MSG_MEDIA_BUTTON: + KeyEvent keyEvent = (KeyEvent) msg.obj; + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + mCallback.onMediaButtonEvent(intent); + break; + case MSG_COMMAND: + Command cmd = (Command) msg.obj; + mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); + break; + case MSG_ADJUST_VOLUME: + adjustVolume((Integer) msg.obj, 0); + break; + case MSG_SET_VOLUME: + setVolumeTo((Integer) msg.obj, 0); + break; + } + } + } + } + + static class MediaSessionImplApi21 implements MediaSessionImpl { + private final Object mSessionObj; + private final Token mToken; + + private PendingIntent mMediaButtonIntent; + + public MediaSessionImplApi21(Context context, String tag) { + mSessionObj = MediaSessionCompatApi21.createSession(context, tag); + mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); + } + + public MediaSessionImplApi21(Object mediaSession) { + mSessionObj = MediaSessionCompatApi21.verifySession(mediaSession); + mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); + } + + @Override + public void setCallback(Callback callback, Handler handler) { + MediaSessionCompatApi21.setCallback(mSessionObj, callback.mCallbackObj, handler); + } + + @Override + public void setFlags(int flags) { + MediaSessionCompatApi21.setFlags(mSessionObj, flags); + } + + @Override + public void setPlaybackToLocal(int stream) { + MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj, + volumeProvider.getVolumeProvider()); + } + + @Override + public void setActive(boolean active) { + MediaSessionCompatApi21.setActive(mSessionObj, active); + } + + @Override + public boolean isActive() { + return MediaSessionCompatApi21.isActive(mSessionObj); + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras); + } + + @Override + public void release() { + MediaSessionCompatApi21.release(mSessionObj); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState()); + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata()); + } + + @Override + public void setSessionActivity(PendingIntent pi) { + MediaSessionCompatApi21.setSessionActivity(mSessionObj, pi); + } + + @Override + public void setMediaButtonReceiver(PendingIntent mbr) { + mMediaButtonIntent = mbr; + MediaSessionCompatApi21.setMediaButtonReceiver(mSessionObj, mbr); + } + + @Override + public void setQueue(List queue) { + List queueObjs = null; + if (queue != null) { + queueObjs = new ArrayList(); + for (QueueItem item : queue) { + queueObjs.add(item.getQueueItem()); + } + } + MediaSessionCompatApi21.setQueue(mSessionObj, queueObjs); + } + + @Override + public void setQueueTitle(CharSequence title) { + MediaSessionCompatApi21.setQueueTitle(mSessionObj, title); + } + + @Override + public void setRatingType(int type) { + if (android.os.Build.VERSION.SDK_INT < 22) { + // TODO figure out 21 implementation + } else { + MediaSessionCompatApi22.setRatingType(mSessionObj, type); + } + } + + @Override + public void setExtras(Bundle extras) { + MediaSessionCompatApi21.setExtras(mSessionObj, extras); + } + + @Override + public Object getMediaSession() { + return mSessionObj; + } + + @Override + public Object getRemoteControlClient() { + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java new file mode 100644 index 00000000000..de644e695d1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java @@ -0,0 +1,250 @@ +/* + * 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. + */ +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; +import android.os.Bundle; +import android.os.ResultReceiver; + +public class MediaSessionCompatApi14 { + /***** RemoteControlClient States, we only need none as the others were public *******/ + final static int RCC_PLAYSTATE_NONE = 0; + + /***** MediaSession States *******/ + final static int STATE_NONE = 0; + final static int STATE_STOPPED = 1; + final static int STATE_PAUSED = 2; + final static int STATE_PLAYING = 3; + final static int STATE_FAST_FORWARDING = 4; + final static int STATE_REWINDING = 5; + final static int STATE_BUFFERING = 6; + final static int STATE_ERROR = 7; + final static int STATE_CONNECTING = 8; + final static int STATE_SKIPPING_TO_PREVIOUS = 9; + final static int STATE_SKIPPING_TO_NEXT = 10; + final static int STATE_SKIPPING_TO_QUEUE_ITEM = 11; + + /***** PlaybackState actions *****/ + private static final long ACTION_STOP = 1 << 0; + private static final long ACTION_PAUSE = 1 << 1; + private static final long ACTION_PLAY = 1 << 2; + private static final long ACTION_REWIND = 1 << 3; + private static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + private static final long ACTION_SKIP_TO_NEXT = 1 << 5; + private static final long ACTION_FAST_FORWARD = 1 << 6; + private static final long ACTION_PLAY_PAUSE = 1 << 9; + + /***** MediaMetadata keys ********/ + private static final String METADATA_KEY_ART = "android.media.metadata.ART"; + private static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + private static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + private static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + private static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + private static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + private static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + private static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + private static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + private static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + private static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + private static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + private static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + private static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + private static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + public static Object createRemoteControlClient(PendingIntent mbIntent) { + return new RemoteControlClient(mbIntent); + } + + public static void setState(Object rccObj, int state) { + ((RemoteControlClient) rccObj).setPlaybackState(getRccStateFromState(state)); + } + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static void setMetadata(Object rccObj, Bundle metadata) { + RemoteControlClient.MetadataEditor editor = ((RemoteControlClient) rccObj).editMetadata( + true); + buildOldMetadata(metadata, editor); + editor.apply(); + } + + public static void registerRemoteControlClient(Context context, Object rccObj) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerRemoteControlClient((RemoteControlClient) rccObj); + } + + public static void unregisterRemoteControlClient(Context context, Object rccObj) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterRemoteControlClient((RemoteControlClient) rccObj); + } + + static int getRccStateFromState(int state) { + switch (state) { + case STATE_CONNECTING: + case STATE_BUFFERING: + return RemoteControlClient.PLAYSTATE_BUFFERING; + case STATE_ERROR: + return RemoteControlClient.PLAYSTATE_ERROR; + case STATE_FAST_FORWARDING: + return RemoteControlClient.PLAYSTATE_FAST_FORWARDING; + case STATE_NONE: + return RCC_PLAYSTATE_NONE; + case STATE_PAUSED: + return RemoteControlClient.PLAYSTATE_PAUSED; + case STATE_PLAYING: + return RemoteControlClient.PLAYSTATE_PLAYING; + case STATE_REWINDING: + return RemoteControlClient.PLAYSTATE_REWINDING; + case STATE_SKIPPING_TO_PREVIOUS: + return RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS; + case STATE_SKIPPING_TO_NEXT: + case STATE_SKIPPING_TO_QUEUE_ITEM: + return RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS; + case STATE_STOPPED: + return RemoteControlClient.PLAYSTATE_STOPPED; + default: + return -1; + } + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = 0; + if ((actions & ACTION_STOP) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_STOP; + } + if ((actions & ACTION_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; + } + if ((actions & ACTION_PLAY) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY; + } + if ((actions & ACTION_REWIND) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_REWIND; + } + if ((actions & ACTION_SKIP_TO_PREVIOUS) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS; + } + if ((actions & ACTION_SKIP_TO_NEXT) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } + if ((actions & ACTION_FAST_FORWARD) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD; + } + if ((actions & ACTION_PLAY_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + return transportControlFlags; + } + + static void buildOldMetadata(Bundle metadata, RemoteControlClient.MetadataEditor editor) { + if (metadata == null) { + return; + } + if (metadata.containsKey(METADATA_KEY_ART)) { + Bitmap art = metadata.getParcelable(METADATA_KEY_ART); + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } else if (metadata.containsKey(METADATA_KEY_ALBUM_ART)) { + // Fall back to album art if the track art wasn't available + Bitmap art = metadata.getParcelable(METADATA_KEY_ALBUM_ART); + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } + if (metadata.containsKey(METADATA_KEY_ALBUM)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + metadata.getString(METADATA_KEY_ALBUM)); + } + if (metadata.containsKey(METADATA_KEY_ALBUM_ARTIST)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, + metadata.getString(METADATA_KEY_ALBUM_ARTIST)); + } + if (metadata.containsKey(METADATA_KEY_ARTIST)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, + metadata.getString(METADATA_KEY_ARTIST)); + } + if (metadata.containsKey(METADATA_KEY_AUTHOR)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_AUTHOR, + metadata.getString(METADATA_KEY_AUTHOR)); + } + if (metadata.containsKey(METADATA_KEY_COMPILATION)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPILATION, + metadata.getString(METADATA_KEY_COMPILATION)); + } + if (metadata.containsKey(METADATA_KEY_COMPOSER)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPOSER, + metadata.getString(METADATA_KEY_COMPOSER)); + } + if (metadata.containsKey(METADATA_KEY_DATE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_DATE, + metadata.getString(METADATA_KEY_DATE)); + } + if (metadata.containsKey(METADATA_KEY_DISC_NUMBER)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, + metadata.getLong(METADATA_KEY_DISC_NUMBER)); + } + if (metadata.containsKey(METADATA_KEY_DURATION)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, + metadata.getLong(METADATA_KEY_DURATION)); + } + if (metadata.containsKey(METADATA_KEY_GENRE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_GENRE, + metadata.getString(METADATA_KEY_GENRE)); + } + if (metadata.containsKey(METADATA_KEY_TITLE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + metadata.getString(METADATA_KEY_TITLE)); + } + if (metadata.containsKey(METADATA_KEY_TRACK_NUMBER)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, + metadata.getLong(METADATA_KEY_TRACK_NUMBER)); + } + if (metadata.containsKey(METADATA_KEY_WRITER)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_WRITER, + metadata.getString(METADATA_KEY_WRITER)); + } + } + + public static interface Callback { + public void onCommand(String command, Bundle extras, ResultReceiver cb); + + public boolean onMediaButtonEvent(Intent mediaButtonIntent); + + public void onPlay(); + + public void onPause(); + + public void onSkipToNext(); + + public void onSkipToPrevious(); + + public void onFastForward(); + + public void onRewind(); + + public void onStop(); + + public void onSeekTo(long pos); + + public void onSetRating(Object ratingObj); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java new file mode 100644 index 00000000000..1c5dca78ed3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java @@ -0,0 +1,93 @@ +/* + * 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. + */ +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.os.SystemClock; + +public class MediaSessionCompatApi18 { + /***** PlaybackState actions *****/ + private static final long ACTION_SEEK_TO = 1 << 8; + + public static Object createPlaybackPositionUpdateListener( + MediaSessionCompatApi14.Callback callback) { + return new OnPlaybackPositionUpdateListener(callback); + } + + public static void registerMediaButtonEventReceiver(Context context, PendingIntent pi) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerMediaButtonEventReceiver(pi); + } + + public static void unregisterMediaButtonEventReceiver(Context context, PendingIntent pi) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterMediaButtonEventReceiver(pi); + } + + public static void setState(Object rccObj, int state, long position, float speed, + long updateTime) { + long currTime = SystemClock.elapsedRealtime(); + if (state == MediaSessionCompatApi14.STATE_PLAYING && position > 0) { + long diff = 0; + if (updateTime > 0) { + diff = currTime - updateTime; + if (speed > 0 && speed != 1f) { + diff *= speed; + } + } + position += diff; + } + state = MediaSessionCompatApi14.getRccStateFromState(state); + ((RemoteControlClient) rccObj).setPlaybackState(state, position, speed); + } + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static void setOnPlaybackPositionUpdateListener(Object rccObj, + Object onPositionUpdateObj) { + ((RemoteControlClient) rccObj).setPlaybackPositionUpdateListener( + (RemoteControlClient.OnPlaybackPositionUpdateListener) onPositionUpdateObj); + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = + MediaSessionCompatApi14.getRccTransportControlFlagsFromActions(actions); + if ((actions & ACTION_SEEK_TO) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; + } + return transportControlFlags; + } + + static class OnPlaybackPositionUpdateListener + implements RemoteControlClient.OnPlaybackPositionUpdateListener { + protected final T mCallback; + + public OnPlaybackPositionUpdateListener(T callback) { + mCallback = callback; + } + + @Override + public void onPlaybackPositionUpdate(long newPositionMs) { + mCallback.onSeekTo(newPositionMs); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java new file mode 100644 index 00000000000..61039d37fcc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java @@ -0,0 +1,101 @@ +/* + * 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. + */ +package android.support.v4.media.session; + +import android.graphics.Bitmap; +import android.media.MediaMetadataEditor; +import android.media.MediaMetadataRetriever; +import android.media.Rating; +import android.media.RemoteControlClient; +import android.os.Bundle; + +public class MediaSessionCompatApi19 { + /***** PlaybackState actions *****/ + private static final long ACTION_SET_RATING = 1 << 7; + + /***** MediaMetadata keys ********/ + private static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + private static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + private static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static Object createMetadataUpdateListener(MediaSessionCompatApi14.Callback callback) { + return new OnMetadataUpdateListener(callback); + } + + public static void setMetadata(Object rccObj, Bundle metadata, long actions) { + RemoteControlClient.MetadataEditor editor = ((RemoteControlClient) rccObj).editMetadata( + true); + MediaSessionCompatApi14.buildOldMetadata(metadata, editor); + addNewMetadata(metadata, editor); + if ((actions & ACTION_SET_RATING) != 0) { + editor.addEditableKey(RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER); + } + editor.apply(); + } + + public static void setOnMetadataUpdateListener(Object rccObj, Object onMetadataUpdateObj) { + ((RemoteControlClient) rccObj).setMetadataUpdateListener( + (RemoteControlClient.OnMetadataUpdateListener) onMetadataUpdateObj); + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = + MediaSessionCompatApi18.getRccTransportControlFlagsFromActions(actions); + if ((actions & ACTION_SET_RATING) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_RATING; + } + return transportControlFlags; + } + + static void addNewMetadata(Bundle metadata, RemoteControlClient.MetadataEditor editor) { + if (metadata == null) { + return; + } + if (metadata.containsKey(METADATA_KEY_YEAR)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_YEAR, + metadata.getLong(METADATA_KEY_YEAR)); + } + if (metadata.containsKey(METADATA_KEY_RATING)) { + editor.putObject(MediaMetadataEditor.RATING_KEY_BY_OTHERS, + metadata.getParcelable(METADATA_KEY_RATING)); + } + if (metadata.containsKey(METADATA_KEY_USER_RATING)) { + editor.putObject(MediaMetadataEditor.RATING_KEY_BY_USER, + metadata.getParcelable(METADATA_KEY_USER_RATING)); + } + } + + static class OnMetadataUpdateListener implements + RemoteControlClient.OnMetadataUpdateListener { + protected final T mCallback; + + public OnMetadataUpdateListener(T callback) { + mCallback = callback; + } + + @Override + public void onMetadataUpdate(int key, Object newValue) { + if (key == MediaMetadataEditor.RATING_KEY_BY_USER && newValue instanceof Rating) { + mCallback.onSetRating(newValue); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java new file mode 100644 index 00000000000..809439137a2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java @@ -0,0 +1,251 @@ +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.AudioAttributes; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.VolumeProvider; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.os.ResultReceiver; + +import java.util.ArrayList; +import java.util.List; + +class MediaSessionCompatApi21 { + public static Object createSession(Context context, String tag) { + return new MediaSession(context, tag); + } + + public static Object verifySession(Object mediaSession) { + if (mediaSession instanceof MediaSession) { + return mediaSession; + } + throw new IllegalArgumentException("mediaSession is not a valid MediaSession object"); + } + + public static Object verifyToken(Object token) { + if (token instanceof MediaSession.Token) { + return token; + } + throw new IllegalArgumentException("token is not a valid MediaSession.Token object"); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static void setCallback(Object sessionObj, Object callbackObj, Handler handler) { + ((MediaSession) sessionObj).setCallback((MediaSession.Callback) callbackObj, handler); + } + + public static void setFlags(Object sessionObj, int flags) { + ((MediaSession)sessionObj).setFlags(flags); + } + + public static void setPlaybackToLocal(Object sessionObj, int stream) { + // TODO update APIs to use support version of AudioAttributes + AudioAttributes.Builder bob = new AudioAttributes.Builder(); + bob.setLegacyStreamType(stream); + ((MediaSession) sessionObj).setPlaybackToLocal(bob.build()); + } + + public static void setPlaybackToRemote(Object sessionObj, Object volumeProviderObj) { + ((MediaSession)sessionObj).setPlaybackToRemote((VolumeProvider)volumeProviderObj); + } + + public static void setActive(Object sessionObj, boolean active) { + ((MediaSession)sessionObj).setActive(active); + } + + public static boolean isActive(Object sessionObj) { + return ((MediaSession)sessionObj).isActive(); + } + + public static void sendSessionEvent(Object sessionObj, String event, Bundle extras) { + ((MediaSession)sessionObj).sendSessionEvent(event, extras); + } + + public static void release(Object sessionObj) { + ((MediaSession)sessionObj).release(); + } + + public static Parcelable getSessionToken(Object sessionObj) { + return ((MediaSession)sessionObj).getSessionToken(); + } + + public static void setPlaybackState(Object sessionObj, Object stateObj) { + ((MediaSession)sessionObj).setPlaybackState((PlaybackState)stateObj); + } + + public static void setMetadata(Object sessionObj, Object metadataObj) { + ((MediaSession)sessionObj).setMetadata((MediaMetadata)metadataObj); + } + + public static void setSessionActivity(Object sessionObj, PendingIntent pi) { + ((MediaSession) sessionObj).setSessionActivity(pi); + } + + public static void setMediaButtonReceiver(Object sessionObj, PendingIntent pi) { + ((MediaSession) sessionObj).setMediaButtonReceiver(pi); + } + + public static void setQueue(Object sessionObj, List queueObjs) { + if (queueObjs == null) { + ((MediaSession) sessionObj).setQueue(null); + return; + } + ArrayList queue = new ArrayList(); + for (Object itemObj : queueObjs) { + queue.add((MediaSession.QueueItem) itemObj); + } + ((MediaSession) sessionObj).setQueue(queue); + } + + public static void setQueueTitle(Object sessionObj, CharSequence title) { + ((MediaSession) sessionObj).setQueueTitle(title); + } + + public static void setExtras(Object sessionObj, Bundle extras) { + ((MediaSession) sessionObj).setExtras(extras); + } + + public static interface Callback { + public void onCommand(String command, Bundle extras, ResultReceiver cb); + public boolean onMediaButtonEvent(Intent mediaButtonIntent); + public void onPlay(); + public void onPlayFromMediaId(String mediaId, Bundle extras); + public void onPlayFromSearch(String search, Bundle extras); + public void onSkipToQueueItem(long id); + public void onPause(); + public void onSkipToNext(); + public void onSkipToPrevious(); + public void onFastForward(); + public void onRewind(); + public void onStop(); + public void onSeekTo(long pos); + public void onSetRating(Object ratingObj); + public void onCustomAction(String action, Bundle extras); + } + + static class CallbackProxy extends MediaSession.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onCommand(String command, Bundle args, ResultReceiver cb) { + mCallback.onCommand(command, args, cb); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return mCallback.onMediaButtonEvent(mediaButtonIntent) + || super.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onPlay() { + mCallback.onPlay(); + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + mCallback.onPlayFromMediaId(mediaId, extras); + } + + @Override + public void onPlayFromSearch(String search, Bundle extras) { + mCallback.onPlayFromSearch(search, extras); + } + + @Override + public void onSkipToQueueItem(long id) { + mCallback.onSkipToQueueItem(id); + } + + @Override + public void onPause() { + mCallback.onPause(); + } + + @Override + public void onSkipToNext() { + mCallback.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + mCallback.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + mCallback.onFastForward(); + } + + @Override + public void onRewind() { + mCallback.onRewind(); + } + + @Override + public void onStop() { + mCallback.onStop(); + } + + @Override + public void onSeekTo(long pos) { + mCallback.onSeekTo(pos); + } + + @Override + public void onSetRating(Rating rating) { + mCallback.onSetRating(rating); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + mCallback.onCustomAction(action, extras); + } + } + + static class QueueItem { + + public static Object createItem(Object mediaDescription, long id) { + return new MediaSession.QueueItem((MediaDescription) mediaDescription, id); + } + + public static Object getDescription(Object queueItem) { + return ((MediaSession.QueueItem) queueItem).getDescription(); + } + + public static long getQueueId(Object queueItem) { + return ((MediaSession.QueueItem) queueItem).getQueueId(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java new file mode 100644 index 00000000000..b847778dfff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java @@ -0,0 +1,25 @@ +/* + * 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. + */ +package android.support.v4.media.session; + +import android.media.session.MediaSession; + +class MediaSessionCompatApi22 { + + public static void setRatingType(Object sessionObj, int type) { + ((MediaSession) sessionObj).setRatingType(type); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java new file mode 100644 index 00000000000..f49eb2b7cb2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java @@ -0,0 +1,32 @@ +/* + * 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. + */ +package android.support.v4.media.session; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; + +public class MediaSessionCompatApi8 { + public static void registerMediaButtonEventReceiver(Context context, ComponentName mbr) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerMediaButtonEventReceiver(mbr); + } + + public static void unregisterMediaButtonEventReceiver(Context context, ComponentName mbr) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterMediaButtonEventReceiver(mbr); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl new file mode 100644 index 00000000000..2e77c4fbde0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable ParcelableVolumeInfo; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java new file mode 100644 index 00000000000..678b33eeb66 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java @@ -0,0 +1,77 @@ +/* Copyright 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. + */ + +package android.support.v4.media.session; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Convenience class for passing information about the audio configuration of a + * {@link MediaSessionCompat}. + */ +public class ParcelableVolumeInfo implements Parcelable { + public int volumeType; + public int audioStream; + public int controlType; + public int maxVolume; + public int currentVolume; + + public ParcelableVolumeInfo(int volumeType, int audioStream, int controlType, + int maxVolume, + int currentVolume) { + this.volumeType = volumeType; + this.audioStream = audioStream; + this.controlType = controlType; + this.maxVolume = maxVolume; + this.currentVolume = currentVolume; + } + + public ParcelableVolumeInfo(Parcel from) { + volumeType = from.readInt(); + controlType = from.readInt(); + maxVolume = from.readInt(); + currentVolume = from.readInt(); + audioStream = from.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(volumeType); + dest.writeInt(controlType); + dest.writeInt(maxVolume); + dest.writeInt(currentVolume); + dest.writeInt(audioStream); + } + + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public ParcelableVolumeInfo createFromParcel(Parcel in) { + return new ParcelableVolumeInfo(in); + } + + @Override + public ParcelableVolumeInfo[] newArray(int size) { + return new ParcelableVolumeInfo[size]; + } + }; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl new file mode 100644 index 00000000000..3d4ef5956a5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 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. +*/ + +package android.support.v4.media.session; + +parcelable PlaybackStateCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java new file mode 100644 index 00000000000..09be98cdcd2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java @@ -0,0 +1,970 @@ +/* + * 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. + */ +package android.support.v4.media.session; + + +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Playback state for a {@link MediaSessionCompat}. This includes a state like + * {@link PlaybackStateCompat#STATE_PLAYING}, the current playback position, + * and the current control capabilities. + */ +public final class PlaybackStateCompat implements Parcelable { + + /** + * Indicates this session supports the stop command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_STOP = 1 << 0; + + /** + * Indicates this session supports the pause command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PAUSE = 1 << 1; + + /** + * Indicates this session supports the play command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY = 1 << 2; + + /** + * Indicates this session supports the rewind command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_REWIND = 1 << 3; + + /** + * Indicates this session supports the previous command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + + /** + * Indicates this session supports the next command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_NEXT = 1 << 5; + + /** + * Indicates this session supports the fast forward command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_FAST_FORWARD = 1 << 6; + + /** + * Indicates this session supports the set rating command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_RATING = 1 << 7; + + /** + * Indicates this session supports the seek to command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SEEK_TO = 1 << 8; + + /** + * Indicates this session supports the play/pause toggle command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_PAUSE = 1 << 9; + + /** + * Indicates this session supports the play from media id command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_MEDIA_ID = 1 << 10; + + /** + * Indicates this session supports the play from search command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_SEARCH = 1 << 11; + + /** + * Indicates this session supports the skip to queue item command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_QUEUE_ITEM = 1 << 12; + + /** + * This is the default playback state and indicates that no media has been + * added yet, or the performer has been reset and has no content to play. + * + * @see Builder#setState + */ + public final static int STATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + * + * @see Builder#setState + */ + public final static int STATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + * + * @see Builder#setState + */ + public final static int STATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + * + * @see Builder#setState + */ + public final static int STATE_PLAYING = 3; + + /** + * State indicating this item is currently fast forwarding. + * + * @see Builder#setState + */ + public final static int STATE_FAST_FORWARDING = 4; + + /** + * State indicating this item is currently rewinding. + * + * @see Builder#setState + */ + public final static int STATE_REWINDING = 5; + + /** + * State indicating this item is currently buffering and will begin playing + * when enough data has buffered. + * + * @see Builder#setState + */ + public final static int STATE_BUFFERING = 6; + + /** + * State indicating this item is currently in an error state. The error + * message should also be set when entering this state. + * + * @see Builder#setState + */ + public final static int STATE_ERROR = 7; + + /** + * State indicating the class doing playback is currently connecting to a + * route. Depending on the implementation you may return to the previous + * state when the connection finishes or enter {@link #STATE_NONE}. If + * the connection failed {@link #STATE_ERROR} should be used. + *

+ * On devices earlier than API 21, this will appear as {@link #STATE_BUFFERING} + *

+ * + * @see Builder#setState + */ + public final static int STATE_CONNECTING = 8; + + /** + * State indicating the player is currently skipping to the previous item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_PREVIOUS = 9; + + /** + * State indicating the player is currently skipping to the next item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_NEXT = 10; + + /** + * State indicating the player is currently skipping to a specific item in + * the queue. + *

+ * On devices earlier than API 21, this will appear as {@link #STATE_SKIPPING_TO_NEXT} + *

+ * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_QUEUE_ITEM = 11; + + /** + * Use this value for the position to indicate the position is not known. + */ + public final static long PLAYBACK_POSITION_UNKNOWN = -1; + + private final int mState; + private final long mPosition; + private final long mBufferedPosition; + private final float mSpeed; + private final long mActions; + private final CharSequence mErrorMessage; + private final long mUpdateTime; + private List mCustomActions; + private final long mActiveItemId; + private final Bundle mExtras; + + private Object mStateObj; + + private PlaybackStateCompat(int state, long position, long bufferedPosition, + float rate, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId, Bundle extras) { + mState = state; + mPosition = position; + mBufferedPosition = bufferedPosition; + mSpeed = rate; + mActions = actions; + mErrorMessage = errorMessage; + mUpdateTime = updateTime; + mCustomActions = new ArrayList<>(customActions); + mActiveItemId = activeItemId; + mExtras = extras; + } + + private PlaybackStateCompat(Parcel in) { + mState = in.readInt(); + mPosition = in.readLong(); + mSpeed = in.readFloat(); + mUpdateTime = in.readLong(); + mBufferedPosition = in.readLong(); + mActions = in.readLong(); + mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mCustomActions = in.createTypedArrayList(CustomAction.CREATOR); + mActiveItemId = in.readLong(); + mExtras = in.readBundle(); + } + + @Override + public String toString() { + StringBuilder bob = new StringBuilder("PlaybackState {"); + bob.append("state=").append(mState); + bob.append(", position=").append(mPosition); + bob.append(", buffered position=").append(mBufferedPosition); + bob.append(", speed=").append(mSpeed); + bob.append(", updated=").append(mUpdateTime); + bob.append(", actions=").append(mActions); + bob.append(", error=").append(mErrorMessage); + bob.append(", custom actions=").append(mCustomActions); + bob.append(", active item id=").append(mActiveItemId); + bob.append("}"); + return bob.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mState); + dest.writeLong(mPosition); + dest.writeFloat(mSpeed); + dest.writeLong(mUpdateTime); + dest.writeLong(mBufferedPosition); + dest.writeLong(mActions); + TextUtils.writeToParcel(mErrorMessage, dest, flags); + dest.writeTypedList(mCustomActions); + dest.writeLong(mActiveItemId); + dest.writeBundle(mExtras); + } + + /** + * Get the current state of playback. One of the following: + *
    + *
  • {@link PlaybackStateCompat#STATE_NONE}
  • + *
  • {@link PlaybackStateCompat#STATE_STOPPED}
  • + *
  • {@link PlaybackStateCompat#STATE_PLAYING}
  • + *
  • {@link PlaybackStateCompat#STATE_PAUSED}
  • + *
  • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
  • + *
  • {@link PlaybackStateCompat#STATE_REWINDING}
  • + *
  • {@link PlaybackStateCompat#STATE_BUFFERING}
  • + *
  • {@link PlaybackStateCompat#STATE_ERROR}
  • + *
  • {@link PlaybackStateCompat#STATE_CONNECTING}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
  • + */ + public int getState() { + return mState; + } + + /** + * Get the current playback position in ms. + */ + public long getPosition() { + return mPosition; + } + + /** + * Get the current buffered position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public long getBufferedPosition() { + return mBufferedPosition; + } + + /** + * Get the current playback speed as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + * + * @return The current speed of playback. + */ + public float getPlaybackSpeed() { + return mSpeed; + } + + /** + * Get the current actions available on this session. This should use a + * bitmask of the available actions. + *
      + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#ACTION_REWIND}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_STOP}
    • + *
    • {@link PlaybackStateCompat#ACTION_FAST_FORWARD}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#ACTION_SEEK_TO}
    • + *
    • {@link PlaybackStateCompat#ACTION_SET_RATING}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}
    • + *
    + */ + public long getActions() { + return mActions; + } + + /** + * Get the list of custom actions. + */ + public List getCustomActions() { + return mCustomActions; + } + + /** + * Get a user readable error message. This should be set when the state is + * {@link PlaybackStateCompat#STATE_ERROR}. + */ + public CharSequence getErrorMessage() { + return mErrorMessage; + } + + /** + * Get the elapsed real time at which position was last updated. If the + * position has never been set this will return 0; + * + * @return The last time the position was updated. + */ + public long getLastPositionUpdateTime() { + return mUpdateTime; + } + + /** + * Get the id of the currently active item in the queue. If there is no + * queue or a queue is not supported by the session this will be + * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID}. + * + * @return The id of the currently active item in the queue or + * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID}. + */ + public long getActiveQueueItemId() { + return mActiveItemId; + } + + /** + * Get any custom extras that were set on this playback state. + * + * @return The extras for this state or null. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + /** + * Creates an instance from a framework {@link android.media.session.PlaybackState} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none. + * @return An equivalent {@link PlaybackStateCompat} object, or null if none. + */ + public static PlaybackStateCompat fromPlaybackState(Object stateObj) { + if (stateObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + List customActionObjs = PlaybackStateCompatApi21.getCustomActions(stateObj); + List customActions = null; + if (customActionObjs != null) { + customActions = new ArrayList<>(customActionObjs.size()); + for (Object customActionObj : customActionObjs) { + customActions.add(CustomAction.fromCustomAction(customActionObj)); + } + } + Bundle extras = Build.VERSION.SDK_INT >= 22 + ? PlaybackStateCompatApi22.getExtras(stateObj) + : null; + PlaybackStateCompat state = new PlaybackStateCompat( + PlaybackStateCompatApi21.getState(stateObj), + PlaybackStateCompatApi21.getPosition(stateObj), + PlaybackStateCompatApi21.getBufferedPosition(stateObj), + PlaybackStateCompatApi21.getPlaybackSpeed(stateObj), + PlaybackStateCompatApi21.getActions(stateObj), + PlaybackStateCompatApi21.getErrorMessage(stateObj), + PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj), + customActions, + PlaybackStateCompatApi21.getActiveQueueItemId(stateObj), + extras); + state.mStateObj = stateObj; + return state; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none. + */ + public Object getPlaybackState() { + if (mStateObj != null || Build.VERSION.SDK_INT < 21) { + return mStateObj; + } + + List customActions = null; + if (mCustomActions != null) { + customActions = new ArrayList<>(mCustomActions.size()); + for (PlaybackStateCompat.CustomAction customAction : mCustomActions) { + customActions.add(customAction.getCustomAction()); + } + } + if (Build.VERSION.SDK_INT >= 22) { + mStateObj = PlaybackStateCompatApi22.newInstance(mState, mPosition, mBufferedPosition, + mSpeed, mActions, mErrorMessage, mUpdateTime, + customActions, mActiveItemId, mExtras); + } else { + mStateObj = PlaybackStateCompatApi21.newInstance(mState, mPosition, mBufferedPosition, + mSpeed, mActions, mErrorMessage, mUpdateTime, + customActions, mActiveItemId); + } + return mStateObj; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public PlaybackStateCompat createFromParcel(Parcel in) { + return new PlaybackStateCompat(in); + } + + @Override + public PlaybackStateCompat[] newArray(int size) { + return new PlaybackStateCompat[size]; + } + }; + + /** + * {@link PlaybackStateCompat.CustomAction CustomActions} can be used to + * extend the capabilities of the standard transport controls by exposing + * app specific actions to {@link MediaControllerCompat Controllers}. + */ + public static final class CustomAction implements Parcelable { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private final Bundle mExtras; + + private Object mCustomActionObj; + + /** + * Use {@link PlaybackStateCompat.CustomAction.Builder#build()}. + */ + private CustomAction(String action, CharSequence name, int icon, Bundle extras) { + mAction = action; + mName = name; + mIcon = icon; + mExtras = extras; + } + + private CustomAction(Parcel in) { + mAction = in.readString(); + mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readInt(); + mExtras = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mAction); + TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mIcon); + dest.writeBundle(mExtras); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creates an instance from a framework + * {@link android.media.session.PlaybackState.CustomAction} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @param customActionObj A {@link android.media.session.PlaybackState.CustomAction} object, + * or null if none. + * @return An equivalent {@link PlaybackStateCompat.CustomAction} object, or null if none. + */ + public static PlaybackStateCompat.CustomAction fromCustomAction(Object customActionObj) { + if (customActionObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction( + PlaybackStateCompatApi21.CustomAction.getAction(customActionObj), + PlaybackStateCompatApi21.CustomAction.getName(customActionObj), + PlaybackStateCompatApi21.CustomAction.getIcon(customActionObj), + PlaybackStateCompatApi21.CustomAction.getExtras(customActionObj)); + customAction.mCustomActionObj = customActionObj; + return customAction; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState.CustomAction} + * object. + *

    + * This method is only supported on API 21+. + *

    + * + * @return An equivalent {@link android.media.session.PlaybackState.CustomAction} object, + * or null if none. + */ + public Object getCustomAction() { + if (mCustomActionObj != null || Build.VERSION.SDK_INT < 21) { + return mCustomActionObj; + } + + mCustomActionObj = PlaybackStateCompatApi21.CustomAction.newInstance(mAction, + mName, mIcon, mExtras); + return mCustomActionObj; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + @Override + public PlaybackStateCompat.CustomAction createFromParcel(Parcel p) { + return new PlaybackStateCompat.CustomAction(p); + } + + @Override + public PlaybackStateCompat.CustomAction[] newArray(int size) { + return new PlaybackStateCompat.CustomAction[size]; + } + }; + + /** + * Returns the action of the {@link CustomAction}. + * + * @return The action of the {@link CustomAction}. + */ + public String getAction() { + return mAction; + } + + /** + * Returns the display name of this action. e.g. "Favorite" + * + * @return The display name of this {@link CustomAction}. + */ + public CharSequence getName() { + return mName; + } + + /** + * Returns the resource id of the icon in the {@link MediaSessionCompat + * Session's} package. + * + * @return The resource id of the icon in the {@link MediaSessionCompat + * Session's} package. + */ + public int getIcon() { + return mIcon; + } + + /** + * Returns extras which provide additional application-specific + * information about the action, or null if none. These arguments are + * meant to be consumed by a {@link MediaControllerCompat} if it knows + * how to handle them. + * + * @return Optional arguments for the {@link CustomAction}. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public String toString() { + return "Action:" + + "mName='" + mName + + ", mIcon=" + mIcon + + ", mExtras=" + mExtras; + } + + /** + * Builder for {@link CustomAction} objects. + */ + public static final class Builder { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private Bundle mExtras; + + /** + * Creates a {@link CustomAction} builder with the id, name, and + * icon set. + * + * @param action The action of the {@link CustomAction}. + * @param name The display name of the {@link CustomAction}. This + * name will be displayed along side the action if the UI + * supports it. + * @param icon The icon resource id of the {@link CustomAction}. + * This resource id must be in the same package as the + * {@link MediaSessionCompat}. It will be displayed with + * the custom action if the UI supports it. + */ + public Builder(String action, CharSequence name, int icon) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException( + "You must specify an action to build a CustomAction."); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException( + "You must specify a name to build a CustomAction."); + } + if (icon == 0) { + throw new IllegalArgumentException( + "You must specify an icon resource id to build a CustomAction."); + } + mAction = action; + mName = name; + mIcon = icon; + } + + /** + * Set optional extras for the {@link CustomAction}. These extras + * are meant to be consumed by a {@link MediaControllerCompat} if it + * knows how to handle them. Keys should be fully qualified (e.g. + * "com.example.MY_ARG") to avoid collisions. + * + * @param extras Optional extras for the {@link CustomAction}. + * @return this. + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Build and return the {@link CustomAction} instance with the + * specified values. + * + * @return A new {@link CustomAction} instance. + */ + public CustomAction build() { + return new CustomAction(mAction, mName, mIcon, mExtras); + } + } + } + + /** + * Builder for {@link PlaybackStateCompat} objects. + */ + public static final class Builder { + private final List mCustomActions = new ArrayList<>(); + + private int mState; + private long mPosition; + private long mBufferedPosition; + private float mRate; + private long mActions; + private CharSequence mErrorMessage; + private long mUpdateTime; + private long mActiveItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + private Bundle mExtras; + + /** + * Create an empty Builder. + */ + public Builder() { + } + + /** + * Create a Builder using a {@link PlaybackStateCompat} instance to set the + * initial values. + * + * @param source The playback state to copy. + */ + public Builder(PlaybackStateCompat source) { + mState = source.mState; + mPosition = source.mPosition; + mRate = source.mSpeed; + mUpdateTime = source.mUpdateTime; + mBufferedPosition = source.mBufferedPosition; + mActions = source.mActions; + mErrorMessage = source.mErrorMessage; + if (source.mCustomActions != null) { + mCustomActions.addAll(source.mCustomActions); + } + mActiveItemId = source.mActiveItemId; + mExtras = source.mExtras; + } + + /** + * Set the current state of playback. + *

    + * The position must be in ms and indicates the current playback + * position within the track. If the position is unknown use + * {@link #PLAYBACK_POSITION_UNKNOWN}. + *

    + * The rate is a multiple of normal playback and should be 0 when paused + * and negative when rewinding. Normal playback rate is 1.0. + *

    + * The state must be one of the following: + *

      + *
    • {@link PlaybackStateCompat#STATE_NONE}
    • + *
    • {@link PlaybackStateCompat#STATE_STOPPED}
    • + *
    • {@link PlaybackStateCompat#STATE_PLAYING}
    • + *
    • {@link PlaybackStateCompat#STATE_PAUSED}
    • + *
    • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
    • + *
    • {@link PlaybackStateCompat#STATE_REWINDING}
    • + *
    • {@link PlaybackStateCompat#STATE_BUFFERING}
    • + *
    • {@link PlaybackStateCompat#STATE_ERROR}
    • + *
    • {@link PlaybackStateCompat#STATE_CONNECTING}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
    • + *
    + * + * @param state The current state of playback. + * @param position The position in the current track in ms. + * @param playbackSpeed The current rate of playback as a multiple of + * normal playback. + */ + public Builder setState(int state, long position, float playbackSpeed) { + return setState(state, position, playbackSpeed, SystemClock.elapsedRealtime()); + } + + /** + * Set the current state of playback. + *

    + * The position must be in ms and indicates the current playback + * position within the track. If the position is unknown use + * {@link #PLAYBACK_POSITION_UNKNOWN}. + *

    + * The rate is a multiple of normal playback and should be 0 when paused + * and negative when rewinding. Normal playback rate is 1.0. + *

    + * The state must be one of the following: + *

      + *
    • {@link PlaybackStateCompat#STATE_NONE}
    • + *
    • {@link PlaybackStateCompat#STATE_STOPPED}
    • + *
    • {@link PlaybackStateCompat#STATE_PLAYING}
    • + *
    • {@link PlaybackStateCompat#STATE_PAUSED}
    • + *
    • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
    • + *
    • {@link PlaybackStateCompat#STATE_REWINDING}
    • + *
    • {@link PlaybackStateCompat#STATE_BUFFERING}
    • + *
    • {@link PlaybackStateCompat#STATE_ERROR}
    • + *
    • {@link PlaybackStateCompat#STATE_CONNECTING}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
    • + *
    + * + * @param state The current state of playback. + * @param position The position in the current item in ms. + * @param playbackSpeed The current speed of playback as a multiple of + * normal playback. + * @param updateTime The time in the {@link SystemClock#elapsedRealtime} + * timebase that the position was updated at. + * @return this + */ + public Builder setState(int state, long position, float playbackSpeed, long updateTime) { + mState = state; + mPosition = position; + mUpdateTime = updateTime; + mRate = playbackSpeed; + return this; + } + + /** + * Set the current buffered position in ms. This is the farthest + * playback point that can be reached from the current position using + * only buffered content. + * + * @return this + */ + public Builder setBufferedPosition(long bufferPosition) { + mBufferedPosition = bufferPosition; + return this; + } + + /** + * Set the current capabilities available on this session. This should + * use a bitmask of the available capabilities. + *
      + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#ACTION_REWIND}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_STOP}
    • + *
    • {@link PlaybackStateCompat#ACTION_FAST_FORWARD}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#ACTION_SEEK_TO}
    • + *
    • {@link PlaybackStateCompat#ACTION_SET_RATING}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}
    • + *
    + * + * @return this + */ + public Builder setActions(long capabilities) { + mActions = capabilities; + return this; + } + + /** + * Add a custom action to the playback state. Actions can be used to + * expose additional functionality to {@link MediaControllerCompat + * Controllers} beyond what is offered by the standard transport + * controls. + *

    + * e.g. start a radio station based on the current item or skip ahead by + * 30 seconds. + * + * @param action An identifier for this action. It can be sent back to + * the {@link MediaSessionCompat} through + * {@link MediaControllerCompat.TransportControls#sendCustomAction(String, Bundle)}. + * @param name The display name for the action. If text is shown with + * the action or used for accessibility, this is what should + * be used. + * @param icon The resource action of the icon that should be displayed + * for the action. The resource should be in the package of + * the {@link MediaSessionCompat}. + * @return this + */ + public Builder addCustomAction(String action, String name, int icon) { + return addCustomAction(new PlaybackStateCompat.CustomAction(action, name, icon, null)); + } + + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaControllerCompat Controllers} beyond what is offered + * by the standard transport controls. + *

    + * An example of an action would be to start a radio station based on the current item + * or to skip ahead by 30 seconds. + * + * @param customAction The custom action to add to the {@link PlaybackStateCompat}. + * @return this + */ + public Builder addCustomAction(PlaybackStateCompat.CustomAction customAction) { + if (customAction == null) { + throw new IllegalArgumentException( + "You may not add a null CustomAction to PlaybackStateCompat."); + } + mCustomActions.add(customAction); + return this; + } + + /** + * Set the active item in the play queue by specifying its id. The + * default value is {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} + * + * @param id The id of the active item. + * @return this + */ + public Builder setActiveQueueItemId(long id) { + mActiveItemId = id; + return this; + } + + /** + * Set a user readable error message. This should be set when the state + * is {@link PlaybackStateCompat#STATE_ERROR}. + * + * @return this + */ + public Builder setErrorMessage(CharSequence errorMessage) { + mErrorMessage = errorMessage; + return this; + } + + /** + * Set any custom extras to be included with the playback state. + * + * @param extras The extras to include. + * @return this + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates the playback state object. + */ + public PlaybackStateCompat build() { + return new PlaybackStateCompat(mState, mPosition, mBufferedPosition, + mRate, mActions, mErrorMessage, mUpdateTime, + mCustomActions, mActiveItemId, mExtras); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java new file mode 100644 index 00000000000..09d3e32407b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java @@ -0,0 +1,103 @@ +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.SystemClock; + +import java.util.ArrayList; +import java.util.List; + +class PlaybackStateCompatApi21 { + public static int getState(Object stateObj) { + return ((PlaybackState)stateObj).getState(); + } + + public static long getPosition(Object stateObj) { + return ((PlaybackState)stateObj).getPosition(); + } + + public static long getBufferedPosition(Object stateObj) { + return ((PlaybackState)stateObj).getBufferedPosition(); + } + + public static float getPlaybackSpeed(Object stateObj) { + return ((PlaybackState)stateObj).getPlaybackSpeed(); + } + + public static long getActions(Object stateObj) { + return ((PlaybackState)stateObj).getActions(); + } + + public static CharSequence getErrorMessage(Object stateObj) { + return ((PlaybackState)stateObj).getErrorMessage(); + } + + public static long getLastPositionUpdateTime(Object stateObj) { + return ((PlaybackState)stateObj).getLastPositionUpdateTime(); + } + + public static List getCustomActions(Object stateObj) { + return (List)((PlaybackState)stateObj).getCustomActions(); + } + + public static long getActiveQueueItemId(Object stateObj) { + return ((PlaybackState)stateObj).getActiveQueueItemId(); + } + + public static Object newInstance(int state, long position, long bufferedPosition, + float speed, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId) { + PlaybackState.Builder stateObj = new PlaybackState.Builder(); + stateObj.setState(state, position, speed, updateTime); + stateObj.setBufferedPosition(bufferedPosition); + stateObj.setActions(actions); + stateObj.setErrorMessage(errorMessage); + for (Object customAction : customActions) { + stateObj.addCustomAction((PlaybackState.CustomAction) customAction); + } + stateObj.setActiveQueueItemId(activeItemId); + return stateObj.build(); + } + + static final class CustomAction { + public static String getAction(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getAction(); + } + + public static CharSequence getName(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getName(); + } + + public static int getIcon(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getIcon(); + } + public static Bundle getExtras(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getExtras(); + } + + public static Object newInstance(String action, CharSequence name, + int icon, Bundle extras) { + PlaybackState.CustomAction.Builder customActionObj = + new PlaybackState.CustomAction.Builder(action, name, icon); + customActionObj.setExtras(extras); + return customActionObj.build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java new file mode 100644 index 00000000000..55d4b835aa7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package android.support.v4.media.session; + +import android.media.session.PlaybackState; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.List; + +class PlaybackStateCompatApi22 { + public static Bundle getExtras(Object stateObj) { + return ((PlaybackState)stateObj).getExtras(); + } + + public static Object newInstance(int state, long position, long bufferedPosition, + float speed, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId, Bundle extras) { + PlaybackState.Builder stateObj = new PlaybackState.Builder(); + stateObj.setState(state, position, speed, updateTime); + stateObj.setBufferedPosition(bufferedPosition); + stateObj.setActions(actions); + stateObj.setErrorMessage(errorMessage); + for (Object customAction : customActions) { + stateObj.addCustomAction((PlaybackState.CustomAction) customAction); + } + stateObj.setActiveQueueItemId(activeItemId); + stateObj.setExtras(extras); + return stateObj.build(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java new file mode 100644 index 00000000000..14f150a6db6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java @@ -0,0 +1,121 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; + +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; + +/** + * Helper for accessing features in {@link ConnectivityManager} introduced after + * API level 16 in a backwards compatible fashion. + */ +public class ConnectivityManagerCompat { + + interface ConnectivityManagerCompatImpl { + boolean isActiveNetworkMetered(ConnectivityManager cm); + } + + static class BaseConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + return true; + case TYPE_WIFI: + return false; + default: + // err on side of caution + return true; + } + } + } + + static class GingerbreadConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatGingerbread.isActiveNetworkMetered(cm); + } + } + + static class HoneycombMR2ConnectivityManagerCompatImpl + implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatHoneycombMR2.isActiveNetworkMetered(cm); + } + } + + static class JellyBeanConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatJellyBean.isActiveNetworkMetered(cm); + } + } + + private static final ConnectivityManagerCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 16) { + IMPL = new JellyBeanConnectivityManagerCompatImpl(); + } else if (Build.VERSION.SDK_INT >= 13) { + IMPL = new HoneycombMR2ConnectivityManagerCompatImpl(); + } else if (Build.VERSION.SDK_INT >= 8) { + IMPL = new GingerbreadConnectivityManagerCompatImpl(); + } else { + IMPL = new BaseConnectivityManagerCompatImpl(); + } + } + + /** + * Returns if the currently active data network is metered. A network is + * classified as metered when the user is sensitive to heavy data usage on + * that connection. You should check this before doing large data transfers, + * and warn the user or delay the operation until another network is + * available. + */ + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + return IMPL.isActiveNetworkMetered(cm); + } + + /** + * Return the {@link NetworkInfo} that caused the given + * {@link ConnectivityManager#CONNECTIVITY_ACTION} broadcast. This obtains + * the current state from {@link ConnectivityManager} instead of using the + * potentially-stale value from + * {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}. + */ + public static NetworkInfo getNetworkInfoFromBroadcast(ConnectivityManager cm, Intent intent) { + final NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + if (info != null) { + return cm.getNetworkInfo(info.getType()); + } else { + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java new file mode 100644 index 00000000000..281aab26266 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java @@ -0,0 +1,57 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIMAX; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Implementation of ConnectivityManagerCompat that can use Gingerbread APIs. + */ +class ConnectivityManagerCompatGingerbread { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_WIMAX: + return true; + case TYPE_WIFI: + return false; + default: + // err on side of caution + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java new file mode 100644 index 00000000000..619c1afc42e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIMAX; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Implementation of ConnectivityManagerCompat that can use Honeycomb MR2 APIs. + */ +class ConnectivityManagerCompatHoneycombMR2 { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_WIMAX: + return true; + case TYPE_WIFI: + case TYPE_BLUETOOTH: + case TYPE_ETHERNET: + return false; + default: + // err on side of caution + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java new file mode 100644 index 00000000000..0795fdd5e63 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import android.net.ConnectivityManager; + +/** + * Implementation of ConnectivityManagerCompat that can use Jellybean APIs. + */ +class ConnectivityManagerCompatJellyBean { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + return cm.isActiveNetworkMetered(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java new file mode 100644 index 00000000000..cfc5c0deac0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java @@ -0,0 +1,203 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import android.os.Build; + +import java.net.Socket; +import java.net.SocketException; + +/** + * Helper for accessing features in TrafficStats introduced after API level 14 + * in a backwards compatible fashion. + */ +public class TrafficStatsCompat { + + interface TrafficStatsCompatImpl { + void clearThreadStatsTag(); + int getThreadStatsTag(); + void incrementOperationCount(int operationCount); + void incrementOperationCount(int tag, int operationCount); + void setThreadStatsTag(int tag); + void tagSocket(Socket socket) throws SocketException; + void untagSocket(Socket socket) throws SocketException; + } + + static class BaseTrafficStatsCompatImpl implements TrafficStatsCompatImpl { + private static class SocketTags { + public int statsTag = -1; + } + + private ThreadLocal mThreadSocketTags = new ThreadLocal() { + @Override + protected SocketTags initialValue() { + return new SocketTags(); + } + }; + + @Override + public void clearThreadStatsTag() { + mThreadSocketTags.get().statsTag = -1; + } + + @Override + public int getThreadStatsTag() { + return mThreadSocketTags.get().statsTag; + } + + @Override + public void incrementOperationCount(int operationCount) { + } + + @Override + public void incrementOperationCount(int tag, int operationCount) { + } + + @Override + public void setThreadStatsTag(int tag) { + mThreadSocketTags.get().statsTag = tag; + } + + @Override + public void tagSocket(Socket socket) { + } + + @Override + public void untagSocket(Socket socket) { + } + } + + static class IcsTrafficStatsCompatImpl implements TrafficStatsCompatImpl { + @Override + public void clearThreadStatsTag() { + TrafficStatsCompatIcs.clearThreadStatsTag(); + } + + @Override + public int getThreadStatsTag() { + return TrafficStatsCompatIcs.getThreadStatsTag(); + } + + @Override + public void incrementOperationCount(int operationCount) { + TrafficStatsCompatIcs.incrementOperationCount(operationCount); + } + + @Override + public void incrementOperationCount(int tag, int operationCount) { + TrafficStatsCompatIcs.incrementOperationCount(tag, operationCount); + } + + @Override + public void setThreadStatsTag(int tag) { + TrafficStatsCompatIcs.setThreadStatsTag(tag); + } + + @Override + public void tagSocket(Socket socket) throws SocketException { + TrafficStatsCompatIcs.tagSocket(socket); + } + + @Override + public void untagSocket(Socket socket) throws SocketException { + TrafficStatsCompatIcs.untagSocket(socket); + } + } + + private static final TrafficStatsCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 14) { + IMPL = new IcsTrafficStatsCompatImpl(); + } else { + IMPL = new BaseTrafficStatsCompatImpl(); + } + } + + /** + * Clear active tag used when accounting {@link Socket} traffic originating + * from the current thread. + */ + public static void clearThreadStatsTag() { + IMPL.clearThreadStatsTag(); + } + + /** + * Get the active tag used when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * {@link #tagSocket(Socket)}. + */ + public static int getThreadStatsTag() { + return IMPL.getThreadStatsTag(); + } + + /** + * Increment count of network operations performed under the accounting tag + * currently active on the calling thread. This can be used to derive + * bytes-per-operation. + * + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int operationCount) { + IMPL.incrementOperationCount(operationCount); + } + + /** + * Increment count of network operations performed under the given + * accounting tag. This can be used to derive bytes-per-operation. + * + * @param tag Accounting tag used in {@link #setThreadStatsTag(int)}. + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int tag, int operationCount) { + IMPL.incrementOperationCount(tag, operationCount); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + *

    + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + *

    + * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and + * used internally by system services like DownloadManager when performing + * traffic on behalf of an application. + */ + public static void setThreadStatsTag(int tag) { + IMPL.setThreadStatsTag(tag); + } + + /** + * Tag the given {@link Socket} with any statistics parameters active for + * the current thread. Subsequent calls always replace any existing + * parameters. When finished, call {@link #untagSocket(Socket)} to remove + * statistics parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagSocket(Socket socket) throws SocketException { + IMPL.tagSocket(socket); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + */ + public static void untagSocket(Socket socket) throws SocketException { + IMPL.untagSocket(socket); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java new file mode 100644 index 00000000000..0f63fceed09 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package android.support.v4.net; + +import android.net.TrafficStats; + +import java.net.Socket; +import java.net.SocketException; + +/** + * Implementation of TrafficStatsCompat that can call ICS APIs. + */ +class TrafficStatsCompatIcs { + public static void clearThreadStatsTag() { + TrafficStats.clearThreadStatsTag(); + } + + public static int getThreadStatsTag() { + return TrafficStats.getThreadStatsTag(); + } + + public static void incrementOperationCount(int operationCount) { + TrafficStats.incrementOperationCount(operationCount); + } + + public static void incrementOperationCount(int tag, int operationCount) { + TrafficStats.incrementOperationCount(tag, operationCount); + } + + public static void setThreadStatsTag(int tag) { + TrafficStats.setThreadStatsTag(tag); + } + + public static void tagSocket(Socket socket) throws SocketException { + TrafficStats.tagSocket(socket); + } + + public static void untagSocket(Socket socket) throws SocketException { + TrafficStats.untagSocket(socket); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java new file mode 100644 index 00000000000..2af8905095b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package android.support.v4.os; + +import android.os.AsyncTask; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.os.AsyncTask} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AsyncTaskCompat { + + /** + * Executes the task with the specified parameters, allowing multiple tasks to run in parallel + * on a pool of threads managed by {@link android.os.AsyncTask}. + * + * @param task The {@link android.os.AsyncTask} to execute. + * @param params The parameters of the task. + * @return the instance of AsyncTask. + */ + public static AsyncTask executeParallel( + AsyncTask task, + Params... params) { + if (task == null) { + throw new IllegalArgumentException("task can not be null"); + } + + if (Build.VERSION.SDK_INT >= 11) { + // From API 11 onwards, we need to manually select the THREAD_POOL_EXECUTOR + AsyncTaskCompatHoneycomb.executeParallel(task, params); + } else { + // Before API 11, all tasks were run in parallel + task.execute(params); + } + + return task; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java new file mode 100644 index 00000000000..5b05a0e404d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +package android.support.v4.os; + +import android.os.AsyncTask; + +/** + * Implementation of AsyncTask compatibility that can call Honeycomb APIs. + */ +class AsyncTaskCompatHoneycomb { + + static void executeParallel( + AsyncTask task, + Params... params) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java new file mode 100644 index 00000000000..66a2a8eb94d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.os; + +import android.os.Build; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Helper for accessing features in {@link Environment} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class EnvironmentCompat { + private static final String TAG = "EnvironmentCompat"; + + /** + * Unknown storage state, such as when a path isn't backed by known storage + * media. + * + * @see #getStorageState(File) + */ + public static final String MEDIA_UNKNOWN = "unknown"; + + /** + * Returns the current state of the storage device that provides the given + * path. + * + * @return one of {@link #MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED}, + * {@link Environment#MEDIA_UNMOUNTED}, + * {@link Environment#MEDIA_CHECKING}, + * {@link Environment#MEDIA_NOFS}, + * {@link Environment#MEDIA_MOUNTED}, + * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, + * {@link Environment#MEDIA_SHARED}, + * {@link Environment#MEDIA_BAD_REMOVAL}, or + * {@link Environment#MEDIA_UNMOUNTABLE}. + */ + public static String getStorageState(File path) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return EnvironmentCompatKitKat.getStorageState(path); + } + + try { + final String canonicalPath = path.getCanonicalPath(); + final String canonicalExternal = Environment.getExternalStorageDirectory() + .getCanonicalPath(); + + if (canonicalPath.startsWith(canonicalExternal)) { + return Environment.getExternalStorageState(); + } + } catch (IOException e) { + Log.w(TAG, "Failed to resolve canonical path: " + e); + } + + return MEDIA_UNKNOWN; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java new file mode 100644 index 00000000000..9a803dbce30 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +package android.support.v4.os; + +import android.os.Environment; + +import java.io.File; + +class EnvironmentCompatKitKat { + public static String getStorageState(File path) { + return Environment.getStorageState(path); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java new file mode 100644 index 00000000000..fbb064f2b53 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Helper for accessing features in {@link android.os.Parcelable} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ParcelableCompat { + + /** + * Factory method for {@link Parcelable.Creator}. + * + * @param callbacks Creator callbacks implementation. + * @return New creator. + */ + public static Parcelable.Creator newCreator( + ParcelableCompatCreatorCallbacks callbacks) { + if (android.os.Build.VERSION.SDK_INT >= 13) { + return ParcelableCompatCreatorHoneycombMR2Stub.instantiate(callbacks); + } + return new CompatCreator(callbacks); + } + + static class CompatCreator implements Parcelable.Creator { + final ParcelableCompatCreatorCallbacks mCallbacks; + + public CompatCreator(ParcelableCompatCreatorCallbacks callbacks) { + mCallbacks = callbacks; + } + + @Override + public T createFromParcel(Parcel source) { + return mCallbacks.createFromParcel(source, null); + } + + @Override + public T[] newArray(int size) { + return mCallbacks.newArray(size); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java new file mode 100644 index 00000000000..a577d6f392f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Callbacks a {@link Parcelable} creator should implement. + */ +public interface ParcelableCompatCreatorCallbacks { + + /** + * Create a new instance of the Parcelable class, instantiating it + * from the given Parcel whose data had previously been written by + * {@link Parcelable#writeToParcel Parcelable.writeToParcel()} and + * using the given ClassLoader. + * + * @param in The Parcel to read the object's data from. + * @param loader The ClassLoader that this object is being created in. + * @return Returns a new instance of the Parcelable class. + */ + public T createFromParcel(Parcel in, ClassLoader loader); + + /** + * Create a new array of the Parcelable class. + * + * @param size Size of the array. + * @return Returns an array of the Parcelable class, with every entry + * initialized to null. + */ + public T[] newArray(int size); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java new file mode 100644 index 00000000000..08acb550708 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.os; + +import android.os.Parcel; +import android.os.Parcelable; + +class ParcelableCompatCreatorHoneycombMR2Stub { + static Parcelable.Creator instantiate(ParcelableCompatCreatorCallbacks callbacks) { + return new ParcelableCompatCreatorHoneycombMR2(callbacks); + } +} + +class ParcelableCompatCreatorHoneycombMR2 implements Parcelable.ClassLoaderCreator { + private final ParcelableCompatCreatorCallbacks mCallbacks; + + public ParcelableCompatCreatorHoneycombMR2(ParcelableCompatCreatorCallbacks callbacks) { + mCallbacks = callbacks; + } + + public T createFromParcel(Parcel in) { + return mCallbacks.createFromParcel(in, null); + } + + public T createFromParcel(Parcel in, ClassLoader loader) { + return mCallbacks.createFromParcel(in, loader); + } + + public T[] newArray(int size) { + return mCallbacks.newArray(size); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java new file mode 100644 index 00000000000..82b0fd5ec48 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java @@ -0,0 +1,60 @@ +/* + * 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.v4.os; + +import android.os.Build; + +/** + * Writes trace events to the system trace buffer. These trace events can be + * collected and visualized using the Systrace tool. + * + *

    This tracing mechanism is independent of the method tracing mechanism + * offered by {@link android.os.Debug#startMethodTracing}. In particular, it enables + * tracing of events that occur across multiple processes. + *

    For information about using the Systrace tool, read Analyzing Display and Performance + * with Systrace. + */ +public class TraceCompat { + /** + * Writes a trace message to indicate that a given section of code has begun. This call must + * be followed by a corresponding call to {@link #endSection()} on the same thread. + * + *

    At this time the vertical bar character '|', newline character '\n', and + * null character '\0' are used internally by the tracing mechanism. If sectionName contains + * these characters they will be replaced with a space character in the trace. + * + * @param sectionName The name of the code section to appear in the trace. This may be at + * most 127 Unicode code units long. + */ + + public static void beginSection(String sectionName) { + if (Build.VERSION.SDK_INT >= 18) { + TraceJellybeanMR2.beginSection(sectionName); + } + } + + /** + * Writes a trace message to indicate that a given section of code has ended. This call must + * be preceeded by a corresponding call to {@link #beginSection(String)}. Calling this method + * will mark the end of the most recently begun section of code, so care must be taken to + * ensure that beginSection / endSection pairs are properly nested and called from the same + * thread. + */ + public static void endSection() { + if (Build.VERSION.SDK_INT >= 18) { + TraceJellybeanMR2.endSection(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java new file mode 100644 index 00000000000..6116947b016 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java @@ -0,0 +1,26 @@ +/* + * 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.v4.os; + +import android.os.Trace; + +class TraceJellybeanMR2 { + public static void beginSection(String section) { + Trace.beginSection(section); + } + + public static void endSection() { + Trace.endSection(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/package.html b/mobile/google-android-support-v4/src/android/support/v4/os/package.html new file mode 100644 index 00000000000..929c967fdc1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/package.html @@ -0,0 +1,6 @@ + + +Support android.os classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java new file mode 100644 index 00000000000..23429ecf140 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java @@ -0,0 +1,353 @@ +/* + * 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. + */ + +package android.support.v4.print; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.net.Uri; + +import java.io.FileNotFoundException; + +/** + * Helper for printing bitmaps. + */ +public final class PrintHelper { + /** + * image will be scaled but leave white space + */ + public static final int SCALE_MODE_FIT = 1; + + /** + * image will fill the paper and be cropped (default) + */ + public static final int SCALE_MODE_FILL = 2; + + /** + * this is a black and white image + */ + public static final int COLOR_MODE_MONOCHROME = 1; + + /** + * this is a color image (default) + */ + public static final int COLOR_MODE_COLOR = 2; + + /** + * Print the image in landscape orientation (default). + */ + public static final int ORIENTATION_LANDSCAPE = 1; + + /** + * Print the image in portrait orientation. + */ + public static final int ORIENTATION_PORTRAIT = 2; + + /** + * Callback for observing when a print operation is completed. + * When print is finished either the system acquired the + * document to print or printing was cancelled. + */ + public interface OnPrintFinishCallback { + + /** + * Called when a print operation is finished. + */ + public void onFinish(); + } + + PrintHelperVersionImpl mImpl; + + /** + * Gets whether the system supports printing. + * + * @return True if printing is supported. + */ + public static boolean systemSupportsPrint() { + if (Build.VERSION.SDK_INT >= 19) { + // Supported on Android 4.4 or later. + return true; + } + return false; + } + + /** + * Interface implemented by classes that support printing + */ + static interface PrintHelperVersionImpl { + + public void setScaleMode(int scaleMode); + + public int getScaleMode(); + + public void setColorMode(int colorMode); + + public int getColorMode(); + + public void setOrientation(int orientation); + + public int getOrientation(); + + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback); + + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) + throws FileNotFoundException; + } + + /** + * Implementation used when we do not support printing + */ + private static final class PrintHelperStubImpl implements PrintHelperVersionImpl { + int mScaleMode = SCALE_MODE_FILL; + int mColorMode = COLOR_MODE_COLOR; + int mOrientation = ORIENTATION_LANDSCAPE; + @Override + public void setScaleMode(int scaleMode) { + mScaleMode = scaleMode; + } + + @Override + public int getColorMode() { + return mColorMode; + } + + @Override + public void setColorMode(int colorMode) { + mColorMode = colorMode; + } + + @Override + public void setOrientation(int orientation) { mOrientation = orientation; } + + @Override + public int getOrientation() { return mOrientation; } + + @Override + public int getScaleMode() { + return mScaleMode; + } + + @Override + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback) { + } + + @Override + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) { + } + } + + /** + * Implementation used on KitKat (and above) + */ + private static final class PrintHelperKitkatImpl implements PrintHelperVersionImpl { + private final PrintHelperKitkat mPrintHelper; + + PrintHelperKitkatImpl(Context context) { + mPrintHelper = new PrintHelperKitkat(context); + } + + @Override + public void setScaleMode(int scaleMode) { + mPrintHelper.setScaleMode(scaleMode); + } + + @Override + public int getScaleMode() { + return mPrintHelper.getScaleMode(); + } + + @Override + public void setColorMode(int colorMode) { + mPrintHelper.setColorMode(colorMode); + } + + @Override + public int getColorMode() { + return mPrintHelper.getColorMode(); + } + + @Override + public void setOrientation(int orientation) { + mPrintHelper.setOrientation(orientation); + } + + @Override + public int getOrientation() { + return mPrintHelper.getOrientation(); + } + + @Override + public void printBitmap(String jobName, Bitmap bitmap, + final OnPrintFinishCallback callback) { + PrintHelperKitkat.OnPrintFinishCallback delegateCallback = null; + if (callback != null) { + delegateCallback = new PrintHelperKitkat.OnPrintFinishCallback() { + @Override + public void onFinish() { + callback.onFinish(); + } + }; + } + mPrintHelper.printBitmap(jobName, bitmap, delegateCallback); + } + + @Override + public void printBitmap(String jobName, Uri imageFile, + final OnPrintFinishCallback callback) throws FileNotFoundException { + PrintHelperKitkat.OnPrintFinishCallback delegateCallback = null; + if (callback != null) { + delegateCallback = new PrintHelperKitkat.OnPrintFinishCallback() { + @Override + public void onFinish() { + callback.onFinish(); + } + }; + } + mPrintHelper.printBitmap(jobName, imageFile, delegateCallback); + } + } + + /** + * Returns the PrintHelper that can be used to print images. + * + * @param context A context for accessing system resources. + * @return the PrintHelper to support printing images. + */ + public PrintHelper(Context context) { + if (systemSupportsPrint()) { + mImpl = new PrintHelperKitkatImpl(context); + } else { + mImpl = new PrintHelperStubImpl(); + } + } + + /** + * Selects whether the image will fill the paper and be cropped + * {@link #SCALE_MODE_FIT} + * or whether the image will be scaled but leave white space + * {@link #SCALE_MODE_FILL}. + * + * @param scaleMode {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public void setScaleMode(int scaleMode) { + mImpl.setScaleMode(scaleMode); + } + + /** + * Returns the scale mode with which the image will fill the paper. + * + * @return The scale Mode: {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public int getScaleMode() { + return mImpl.getScaleMode(); + } + + /** + * Sets whether the image will be printed in color (default) + * {@link #COLOR_MODE_COLOR} or in back and white + * {@link #COLOR_MODE_MONOCHROME}. + * + * @param colorMode The color mode which is one of + * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}. + */ + public void setColorMode(int colorMode) { + mImpl.setColorMode(colorMode); + } + + /** + * Gets the color mode with which the image will be printed. + * + * @return The color mode which is one of {@link #COLOR_MODE_COLOR} + * and {@link #COLOR_MODE_MONOCHROME}. + */ + public int getColorMode() { + return mImpl.getColorMode(); + } + + /** + * Sets whether the image will be printed in landscape {@link #ORIENTATION_LANDSCAPE} (default) + * or portrait {@link #ORIENTATION_PORTRAIT}. + * + * @param orientation The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public void setOrientation(int orientation) { + mImpl.setOrientation(orientation); + } + + /** + * Gets whether the image will be printed in landscape or portrait. + * + * @return The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public int getOrientation() { + return mImpl.getOrientation(); + } + + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + */ + public void printBitmap(String jobName, Bitmap bitmap) { + mImpl.printBitmap(jobName, bitmap, null); + } + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback) { + mImpl.printBitmap(jobName, bitmap, callback); + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)} + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + */ + public void printBitmap(String jobName, Uri imageFile) throws FileNotFoundException { + mImpl.printBitmap(jobName, imageFile, null); + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)} + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) + throws FileNotFoundException { + mImpl.printBitmap(jobName, imageFile, callback); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java new file mode 100644 index 00000000000..b827b1f3a91 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java @@ -0,0 +1,544 @@ +/* + * 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. + */ + +package android.support.v4.print; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.pdf.PdfDocument.Page; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentInfo; +import android.print.PrintManager; +import android.print.pdf.PrintedPdfDocument; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Kitkat specific PrintManager API implementation. + */ +class PrintHelperKitkat { + private static final String LOG_TAG = "PrintHelperKitkat"; + // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi) + private final static int MAX_PRINT_SIZE = 3500; + final Context mContext; + BitmapFactory.Options mDecodeOptions = null; + private final Object mLock = new Object(); + /** + * image will be scaled but leave white space + */ + public static final int SCALE_MODE_FIT = 1; + /** + * image will fill the paper and be cropped (default) + */ + public static final int SCALE_MODE_FILL = 2; + + /** + * select landscape (default) + */ + public static final int ORIENTATION_LANDSCAPE = 1; + + /** + * select portrait + */ + public static final int ORIENTATION_PORTRAIT = 2; + + /** + * this is a black and white image + */ + public static final int COLOR_MODE_MONOCHROME = 1; + /** + * this is a color image (default) + */ + public static final int COLOR_MODE_COLOR = 2; + + public interface OnPrintFinishCallback { + public void onFinish(); + } + + int mScaleMode = SCALE_MODE_FILL; + + int mColorMode = COLOR_MODE_COLOR; + + int mOrientation = ORIENTATION_LANDSCAPE; + + PrintHelperKitkat(Context context) { + mContext = context; + } + + /** + * Selects whether the image will fill the paper and be cropped + *

    + * {@link #SCALE_MODE_FIT} + * or whether the image will be scaled but leave white space + * {@link #SCALE_MODE_FILL}. + * + * @param scaleMode {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public void setScaleMode(int scaleMode) { + mScaleMode = scaleMode; + } + + /** + * Returns the scale mode with which the image will fill the paper. + * + * @return The scale Mode: {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public int getScaleMode() { + return mScaleMode; + } + + /** + * Sets whether the image will be printed in color (default) + * {@link #COLOR_MODE_COLOR} or in back and white + * {@link #COLOR_MODE_MONOCHROME}. + * + * @param colorMode The color mode which is one of + * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}. + */ + public void setColorMode(int colorMode) { + mColorMode = colorMode; + } + + /** + * Sets whether to select landscape (default), {@link #ORIENTATION_LANDSCAPE} + * or portrait {@link #ORIENTATION_PORTRAIT} + * @param orientation The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + /** + * Gets the page orientation with which the image will be printed. + * + * @return The preferred orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT} + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Gets the color mode with which the image will be printed. + * + * @return The color mode which is one of {@link #COLOR_MODE_COLOR} + * and {@link #COLOR_MODE_MONOCHROME}. + */ + public int getColorMode() { + return mColorMode; + } + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(final String jobName, final Bitmap bitmap, + final OnPrintFinishCallback callback) { + if (bitmap == null) { + return; + } + final int fittingMode = mScaleMode; // grab the fitting mode at time of call + PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); + PrintAttributes.MediaSize mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT; + if (bitmap.getWidth() > bitmap.getHeight()) { + mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE; + } + PrintAttributes attr = new PrintAttributes.Builder() + .setMediaSize(mediaSize) + .setColorMode(mColorMode) + .build(); + + printManager.print(jobName, + new PrintDocumentAdapter() { + private PrintAttributes mAttributes; + + @Override + public void onLayout(PrintAttributes oldPrintAttributes, + PrintAttributes newPrintAttributes, + CancellationSignal cancellationSignal, + LayoutResultCallback layoutResultCallback, + Bundle bundle) { + + mAttributes = newPrintAttributes; + + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + layoutResultCallback.onLayoutFinished(info, changed); + } + + @Override + public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, + CancellationSignal cancellationSignal, + WriteResultCallback writeResultCallback) { + PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, + mAttributes); + try { + Page page = pdfDocument.startPage(1); + + RectF content = new RectF(page.getInfo().getContentRect()); + + Matrix matrix = getMatrix(bitmap.getWidth(), bitmap.getHeight(), + content, fittingMode); + + // Draw the bitmap. + page.getCanvas().drawBitmap(bitmap, matrix, null); + + // Finish the page. + pdfDocument.finishPage(page); + + try { + // Write the document. + pdfDocument.writeTo(new FileOutputStream( + fileDescriptor.getFileDescriptor())); + // Done. + writeResultCallback.onWriteFinished( + new PageRange[]{PageRange.ALL_PAGES}); + } catch (IOException ioe) { + // Failed. + Log.e(LOG_TAG, "Error writing printed content", ioe); + writeResultCallback.onWriteFailed(null); + } + } finally { + if (pdfDocument != null) { + pdfDocument.close(); + } + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + + @Override + public void onFinish() { + if (callback != null) { + callback.onFinish(); + } + } + }, attr); + } + + /** + * Calculates the transform the print an Image to fill the page + * + * @param imageWidth with of bitmap + * @param imageHeight height of bitmap + * @param content The output page dimensions + * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs {@link #SCALE_MODE_FIT} + * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call + */ + private Matrix getMatrix(int imageWidth, int imageHeight, RectF content, int fittingMode) { + Matrix matrix = new Matrix(); + + // Compute and apply scale to fill the page. + float scale = content.width() / imageWidth; + if (fittingMode == SCALE_MODE_FILL) { + scale = Math.max(scale, content.height() / imageHeight); + } else { + scale = Math.min(scale, content.height() / imageHeight); + } + matrix.postScale(scale, scale); + + // Center the content. + final float translateX = (content.width() + - imageWidth * scale) / 2; + final float translateY = (content.height() + - imageHeight * scale) / 2; + matrix.postTranslate(translateX, translateY); + return matrix; + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * BitmapFactory.decodeStream (JPEG, GIF, PNG, BMP, WEBP) + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @param callback Optional callback to observe when printing is finished. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + */ + public void printBitmap(final String jobName, final Uri imageFile, + final OnPrintFinishCallback callback) throws FileNotFoundException { + final int fittingMode = mScaleMode; + + PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() { + private PrintAttributes mAttributes; + AsyncTask mLoadBitmap; + Bitmap mBitmap = null; + + @Override + public void onLayout(final PrintAttributes oldPrintAttributes, + final PrintAttributes newPrintAttributes, + final CancellationSignal cancellationSignal, + final LayoutResultCallback layoutResultCallback, + Bundle bundle) { + + mAttributes = newPrintAttributes; + + if (cancellationSignal.isCanceled()) { + layoutResultCallback.onLayoutCancelled(); + return; + } + // we finished the load + if (mBitmap != null) { + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + layoutResultCallback.onLayoutFinished(info, changed); + return; + } + + mLoadBitmap = new AsyncTask() { + + @Override + protected void onPreExecute() { + // First register for cancellation requests. + cancellationSignal.setOnCancelListener( + new CancellationSignal.OnCancelListener() { + @Override + public void onCancel() { // on different thread + cancelLoad(); + cancel(false); + } + }); + } + + @Override + protected Bitmap doInBackground(Uri... uris) { + try { + return loadConstrainedBitmap(imageFile, MAX_PRINT_SIZE); + } catch (FileNotFoundException e) { + /* ignore */ + } + return null; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + super.onPostExecute(bitmap); + mBitmap = bitmap; + if (bitmap != null) { + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + + layoutResultCallback.onLayoutFinished(info, changed); + + } else { + layoutResultCallback.onLayoutFailed(null); + } + mLoadBitmap = null; + } + + @Override + protected void onCancelled(Bitmap result) { + // Task was cancelled, report that. + layoutResultCallback.onLayoutCancelled(); + mLoadBitmap = null; + } + }.execute(); + } + + private void cancelLoad() { + synchronized (mLock) { // prevent race with set null below + if (mDecodeOptions != null) { + mDecodeOptions.requestCancelDecode(); + mDecodeOptions = null; + } + } + } + + @Override + public void onFinish() { + super.onFinish(); + cancelLoad(); + if (mLoadBitmap != null) { + mLoadBitmap.cancel(true); + } + if (callback != null) { + callback.onFinish(); + } + } + + @Override + public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, + CancellationSignal cancellationSignal, + WriteResultCallback writeResultCallback) { + PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, + mAttributes); + try { + + Page page = pdfDocument.startPage(1); + RectF content = new RectF(page.getInfo().getContentRect()); + + // Compute and apply scale to fill the page. + Matrix matrix = getMatrix(mBitmap.getWidth(), mBitmap.getHeight(), + content, fittingMode); + + // Draw the bitmap. + page.getCanvas().drawBitmap(mBitmap, matrix, null); + + // Finish the page. + pdfDocument.finishPage(page); + + try { + // Write the document. + pdfDocument.writeTo(new FileOutputStream( + fileDescriptor.getFileDescriptor())); + // Done. + writeResultCallback.onWriteFinished( + new PageRange[]{PageRange.ALL_PAGES}); + } catch (IOException ioe) { + // Failed. + Log.e(LOG_TAG, "Error writing printed content", ioe); + writeResultCallback.onWriteFailed(null); + } + } finally { + if (pdfDocument != null) { + pdfDocument.close(); + } + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + }; + + PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + builder.setColorMode(mColorMode); + + if (mOrientation == ORIENTATION_LANDSCAPE) { + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE); + } else if (mOrientation == ORIENTATION_PORTRAIT) { + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT); + } + PrintAttributes attr = builder.build(); + + printManager.print(jobName, printDocumentAdapter, attr); + } + + /** + * Loads a bitmap while limiting its size + * + * @param uri location of a valid image + * @param maxSideLength the maximum length of a size + * @return the Bitmap + * @throws FileNotFoundException if the Uri does not point to an image + */ + private Bitmap loadConstrainedBitmap(Uri uri, int maxSideLength) throws FileNotFoundException { + if (maxSideLength <= 0 || uri == null || mContext == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + // Get width and height of stored bitmap + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + loadBitmap(uri, opt); + + int w = opt.outWidth; + int h = opt.outHeight; + + // If bitmap cannot be decoded, return null + if (w <= 0 || h <= 0) { + return null; + } + + // Find best downsampling size + int imageSide = Math.max(w, h); + + int sampleSize = 1; + while (imageSide > maxSideLength) { + imageSide >>>= 1; + sampleSize <<= 1; + } + + // Make sure sample size is reasonable + if (sampleSize <= 0 || 0 >= (int) (Math.min(w, h) / sampleSize)) { + return null; + } + BitmapFactory.Options decodeOptions = null; + synchronized (mLock) { // prevent race with set null below + mDecodeOptions = new BitmapFactory.Options(); + mDecodeOptions.inMutable = true; + mDecodeOptions.inSampleSize = sampleSize; + decodeOptions = mDecodeOptions; + } + try { + return loadBitmap(uri, decodeOptions); + } finally { + synchronized (mLock) { + mDecodeOptions = null; + } + } + } + + /** + * Returns the bitmap from the given uri loaded using the given options. + * Returns null on failure. + */ + private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException { + if (uri == null || mContext == null) { + throw new IllegalArgumentException("bad argument to loadBitmap"); + } + InputStream is = null; + try { + is = mContext.getContentResolver().openInputStream(uri); + return BitmapFactory.decodeStream(is, null, o); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException t) { + Log.w(LOG_TAG, "close fail ", t); + } + } + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java new file mode 100644 index 00000000000..0529da8ff27 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java @@ -0,0 +1,345 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; + +import java.io.File; + +/** + * Representation of a document backed by either a + * {@link android.provider.DocumentsProvider} or a raw file on disk. This is a + * utility class designed to emulate the traditional {@link File} interface. It + * offers a simplified view of a tree of documents, but it has substantial + * overhead. For optimal performance and a richer feature set, use the + * {@link android.provider.DocumentsContract} methods and constants directly. + *

    + * There are several differences between documents and traditional files: + *

      + *
    • Documents express their display name and MIME type as separate fields, + * instead of relying on file extensions. Some documents providers may still + * choose to append extensions to their display names, but that's an + * implementation detail. + *
    • A single document may appear as the child of multiple directories, so it + * doesn't inherently know who its parent is. That is, documents don't have a + * strong notion of path. You can easily traverse a tree of documents from + * parent to child, but not from child to parent. + *
    • Each document has a unique identifier within that provider. This + * identifier is an opaque implementation detail of the provider, and + * as such it must not be parsed. + *
    + *

    + * Before using this class, first consider if you really need access to an + * entire subtree of documents. The principle of least privilege dictates that + * you should only ask for access to documents you really need. If you only need + * the user to pick a single file, use {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_GET_CONTENT}. If you want to let the user pick multiple + * files, add {@link Intent#EXTRA_ALLOW_MULTIPLE}. If you only need the user to + * save a single file, use {@link Intent#ACTION_CREATE_DOCUMENT}. If you use + * these APIs, you can pass the resulting {@link Intent#getData()} into + * {@link #fromSingleUri(Context, Uri)} to work with that document. + *

    + * If you really do need full access to an entire subtree of documents, start by + * launching {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to let the user pick a + * directory. Then pass the resulting {@link Intent#getData()} into + * {@link #fromTreeUri(Context, Uri)} to start working with the user selected + * tree. + *

    + * As you navigate the tree of DocumentFile instances, you can always use + * {@link #getUri()} to obtain the Uri representing the underlying document for + * that object, for use with {@link ContentResolver#openInputStream(Uri)}, etc. + *

    + * To simplify your code on devices running + * {@link android.os.Build.VERSION_CODES#KITKAT} or earlier, you can use + * {@link #fromFile(File)} which emulates the behavior of a + * {@link android.provider.DocumentsProvider}. + * + * @see android.provider.DocumentsProvider + * @see android.provider.DocumentsContract + */ +public abstract class DocumentFile { + static final String TAG = "DocumentFile"; + + private final DocumentFile mParent; + + DocumentFile(DocumentFile parent) { + mParent = parent; + } + + /** + * Create a {@link DocumentFile} representing the filesystem tree rooted at + * the given {@link Uri}. This doesn't give you any additional access to the + * underlying files beyond what your app already has. + *

    + * {@link #getUri()} will return {@code file://} Uris for files explored + * through this tree. + */ + public static DocumentFile fromFile(File file) { + return new RawDocumentFile(null, file); + } + + /** + * Create a {@link DocumentFile} representing the single document at the + * given {@link Uri}. This is only useful on devices running + * {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param singleUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_CREATE_DOCUMENT} request. + */ + public static DocumentFile fromSingleUri(Context context, Uri singleUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return new SingleDocumentFile(null, context, singleUri); + } else { + return null; + } + } + + /** + * Create a {@link DocumentFile} representing the document tree rooted at + * the given {@link Uri}. This is only useful on devices running + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param treeUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request. + */ + public static DocumentFile fromTreeUri(Context context, Uri treeUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return new TreeDocumentFile(null, context, + DocumentsContractApi21.prepareTreeUri(treeUri)); + } else { + return null; + } + } + + /** + * Test if given Uri is backed by a + * {@link android.provider.DocumentsProvider}. + */ + public static boolean isDocumentUri(Context context, Uri uri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return DocumentsContractApi19.isDocumentUri(context, uri); + } else { + return false; + } + } + + /** + * Create a new document as a direct child of this directory. + * + * @param mimeType MIME type of new document, such as {@code image/png} or + * {@code audio/flac} + * @param displayName name of new document, without any file extension + * appended; the underlying provider may choose to append the + * extension + * @return file representing newly created document, or null if failed + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract DocumentFile createFile(String mimeType, String displayName); + + /** + * Create a new directory as a direct child of this directory. + * + * @param displayName name of new directory + * @return file representing newly created directory, or null if failed + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract DocumentFile createDirectory(String displayName); + + /** + * Return a Uri for the underlying document represented by this file. This + * can be used with other platform APIs to manipulate or share the + * underlying content. You can use {@link #isDocumentUri(Context, Uri)} to + * test if the returned Uri is backed by a + * {@link android.provider.DocumentsProvider}. + * + * @see Intent#setData(Uri) + * @see Intent#setClipData(android.content.ClipData) + * @see ContentResolver#openInputStream(Uri) + * @see ContentResolver#openOutputStream(Uri) + * @see ContentResolver#openFileDescriptor(Uri, String) + */ + public abstract Uri getUri(); + + /** + * Return the display name of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_DISPLAY_NAME + */ + public abstract String getName(); + + /** + * Return the MIME type of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract String getType(); + + /** + * Return the parent file of this document. Only defined inside of the + * user-selected tree; you can never escape above the top of the tree. + *

    + * The underlying {@link android.provider.DocumentsProvider} only defines a + * forward mapping from parent to child, so the reverse mapping of child to + * parent offered here is purely a convenience method, and it may be + * incorrect if the underlying tree structure changes. + */ + public DocumentFile getParentFile() { + return mParent; + } + + /** + * Indicates if this file represents a directory. + * + * @return {@code true} if this file is a directory, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#MIME_TYPE_DIR + */ + public abstract boolean isDirectory(); + + /** + * Indicates if this file represents a file. + * + * @return {@code true} if this file is a file, {@code false} otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract boolean isFile(); + + /** + * Returns the time when this file was last modified, measured in + * milliseconds since January 1st, 1970, midnight. Returns 0 if the file + * does not exist, or if the modified time is unknown. + * + * @return the time when this file was last modified. + * @see android.provider.DocumentsContract.Document#COLUMN_LAST_MODIFIED + */ + public abstract long lastModified(); + + /** + * Returns the length of this file in bytes. Returns 0 if the file does not + * exist, or if the length is unknown. The result for a directory is not + * defined. + * + * @return the number of bytes in this file. + * @see android.provider.DocumentsContract.Document#COLUMN_SIZE + */ + public abstract long length(); + + /** + * Indicates whether the current context is allowed to read from this file. + * + * @return {@code true} if this file can be read, {@code false} otherwise. + */ + public abstract boolean canRead(); + + /** + * Indicates whether the current context is allowed to write to this file. + * + * @return {@code true} if this file can be written, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_FLAGS + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE + * @see android.provider.DocumentsContract.Document#FLAG_DIR_SUPPORTS_CREATE + */ + public abstract boolean canWrite(); + + /** + * Deletes this file. + *

    + * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + * + * @return {@code true} if this file was deleted, {@code false} otherwise. + * @see android.provider.DocumentsContract#deleteDocument(ContentResolver, + * Uri) + */ + public abstract boolean delete(); + + /** + * Returns a boolean indicating whether this file can be found. + * + * @return {@code true} if this file exists, {@code false} otherwise. + */ + public abstract boolean exists(); + + /** + * Returns an array of files contained in the directory represented by this + * file. + * + * @return an array of files or {@code null}. + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#buildChildDocumentsUriUsingTree(Uri, + * String) + */ + public abstract DocumentFile[] listFiles(); + + /** + * Search through {@link #listFiles()} for the first document matching the + * given display name. Returns {@code null} when no matching document is + * found. + * + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + */ + public DocumentFile findFile(String displayName) { + for (DocumentFile doc : listFiles()) { + if (displayName.equals(doc.getName())) { + return doc; + } + } + return null; + } + + /** + * Renames this file to {@code displayName}. + *

    + * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + *

    + * Some providers may need to create a new document to reflect the rename, + * potentially with a different MIME type, so {@link #getUri()} and + * {@link #getType()} may change to reflect the rename. + *

    + * When renaming a directory, children previously enumerated through + * {@link #listFiles()} may no longer be valid. + * + * @param displayName the new display name. + * @return true on success. + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#renameDocument(ContentResolver, + * Uri, String) + */ + public abstract boolean renameTo(String displayName); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java new file mode 100644 index 00000000000..3905e39ff8d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java @@ -0,0 +1,197 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; + +class DocumentsContractApi19 { + private static final String TAG = "DocumentFile"; + + public static boolean isDocumentUri(Context context, Uri self) { + return DocumentsContract.isDocumentUri(context, self); + } + + public static String getName(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_DISPLAY_NAME, null); + } + + private static String getRawType(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_MIME_TYPE, null); + } + + public static String getType(Context context, Uri self) { + final String rawType = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(rawType)) { + return null; + } else { + return rawType; + } + } + + public static boolean isDirectory(Context context, Uri self) { + return DocumentsContract.Document.MIME_TYPE_DIR.equals(getRawType(context, self)); + } + + public static boolean isFile(Context context, Uri self) { + final String type = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) || TextUtils.isEmpty(type)) { + return false; + } else { + return true; + } + } + + public static long lastModified(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0); + } + + public static long length(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, 0); + } + + public static boolean canRead(Context context, Uri self) { + // Ignore if grant doesn't allow read + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + // Ignore documents without MIME + if (TextUtils.isEmpty(getRawType(context, self))) { + return false; + } + + return true; + } + + public static boolean canWrite(Context context, Uri self) { + // Ignore if grant doesn't allow write + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + final String type = getRawType(context, self); + final int flags = queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0); + + // Ignore documents without MIME + if (TextUtils.isEmpty(type)) { + return false; + } + + // Deletable documents considered writable + if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) { + return true; + } + + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) + && (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { + // Directories that allow create considered writable + return true; + } else if (!TextUtils.isEmpty(type) + && (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) { + // Writable normal files considered writable + return true; + } + + return false; + } + + public static boolean delete(Context context, Uri self) { + return DocumentsContract.deleteDocument(context.getContentResolver(), self); + } + + public static boolean exists(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + return c.getCount() > 0; + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return false; + } finally { + closeQuietly(c); + } + } + + private static String queryForString(Context context, Uri self, String column, + String defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getString(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static int queryForInt(Context context, Uri self, String column, + int defaultValue) { + return (int) queryForLong(context, self, column, defaultValue); + } + + private static long queryForLong(Context context, Uri self, String column, + long defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getLong(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java new file mode 100644 index 00000000000..b583a0d08e3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java @@ -0,0 +1,85 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.util.Log; + +import java.util.ArrayList; + +class DocumentsContractApi21 { + private static final String TAG = "DocumentFile"; + + public static Uri createFile(Context context, Uri self, String mimeType, + String displayName) { + return DocumentsContract.createDocument(context.getContentResolver(), self, mimeType, + displayName); + } + + public static Uri createDirectory(Context context, Uri self, String displayName) { + return createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName); + } + + public static Uri prepareTreeUri(Uri treeUri) { + return DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)); + } + + public static Uri[] listFiles(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, + DocumentsContract.getDocumentId(self)); + final ArrayList results = new ArrayList(); + + Cursor c = null; + try { + c = resolver.query(childrenUri, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + while (c.moveToNext()) { + final String documentId = c.getString(0); + final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(self, + documentId); + results.add(documentUri); + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + } finally { + closeQuietly(c); + } + + return results.toArray(new Uri[results.size()]); + } + + public static Uri renameTo(Context context, Uri self, String displayName) { + return DocumentsContract.renameDocument(context.getContentResolver(), self, displayName); + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java new file mode 100644 index 00000000000..9dc97c65a02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java @@ -0,0 +1,174 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +class RawDocumentFile extends DocumentFile { + private File mFile; + + RawDocumentFile(DocumentFile parent, File file) { + super(parent); + mFile = file; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + // Tack on extension when valid MIME type provided + final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extension != null) { + displayName += "." + extension; + } + final File target = new File(mFile, displayName); + try { + target.createNewFile(); + return new RawDocumentFile(this, target); + } catch (IOException e) { + Log.w(TAG, "Failed to createFile: " + e); + return null; + } + } + + @Override + public DocumentFile createDirectory(String displayName) { + final File target = new File(mFile, displayName); + if (target.isDirectory() || target.mkdir()) { + return new RawDocumentFile(this, target); + } else { + return null; + } + } + + @Override + public Uri getUri() { + return Uri.fromFile(mFile); + } + + @Override + public String getName() { + return mFile.getName(); + } + + @Override + public String getType() { + if (mFile.isDirectory()) { + return null; + } else { + return getTypeForName(mFile.getName()); + } + } + + @Override + public boolean isDirectory() { + return mFile.isDirectory(); + } + + @Override + public boolean isFile() { + return mFile.isFile(); + } + + @Override + public long lastModified() { + return mFile.lastModified(); + } + + @Override + public long length() { + return mFile.length(); + } + + @Override + public boolean canRead() { + return mFile.canRead(); + } + + @Override + public boolean canWrite() { + return mFile.canWrite(); + } + + @Override + public boolean delete() { + deleteContents(mFile); + return mFile.delete(); + } + + @Override + public boolean exists() { + return mFile.exists(); + } + + @Override + public DocumentFile[] listFiles() { + final ArrayList results = new ArrayList(); + final File[] files = mFile.listFiles(); + if (files != null) { + for (File file : files) { + results.add(new RawDocumentFile(this, file)); + } + } + return results.toArray(new DocumentFile[results.size()]); + } + + @Override + public boolean renameTo(String displayName) { + final File target = new File(mFile.getParentFile(), displayName); + if (mFile.renameTo(target)) { + mFile = target; + return true; + } else { + return false; + } + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + private static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete " + file); + success = false; + } + } + } + return success; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java new file mode 100644 index 00000000000..673d1dd00e2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.content.Context; +import android.net.Uri; +import android.support.v4.provider.DocumentsContractApi19; + +class SingleDocumentFile extends DocumentFile { + private Context mContext; + private Uri mUri; + + SingleDocumentFile(DocumentFile parent, Context context, Uri uri) { + super(parent); + mContext = context; + mUri = uri; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + throw new UnsupportedOperationException(); + } + + @Override + public DocumentFile createDirectory(String displayName) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public DocumentFile[] listFiles() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean renameTo(String displayName) { + throw new UnsupportedOperationException(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java new file mode 100644 index 00000000000..96983c06ba9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java @@ -0,0 +1,119 @@ +/* + * 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. + */ + +package android.support.v4.provider; + +import android.content.Context; +import android.net.Uri; + +class TreeDocumentFile extends DocumentFile { + private Context mContext; + private Uri mUri; + + TreeDocumentFile(DocumentFile parent, Context context, Uri uri) { + super(parent); + mContext = context; + mUri = uri; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + final Uri result = DocumentsContractApi21.createFile(mContext, mUri, mimeType, displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result) : null; + } + + @Override + public DocumentFile createDirectory(String displayName) { + final Uri result = DocumentsContractApi21.createDirectory(mContext, mUri, displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result) : null; + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public DocumentFile[] listFiles() { + final Uri[] result = DocumentsContractApi21.listFiles(mContext, mUri); + final DocumentFile[] resultFiles = new DocumentFile[result.length]; + for (int i = 0; i < result.length; i++) { + resultFiles[i] = new TreeDocumentFile(this, mContext, result[i]); + } + return resultFiles; + } + + @Override + public boolean renameTo(String displayName) { + final Uri result = DocumentsContractApi21.renameTo(mContext, mUri, displayName); + if (result != null) { + mUri = result; + return true; + } else { + return false; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java new file mode 100644 index 00000000000..4e0c041c6b4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java @@ -0,0 +1,27 @@ +package android.support.v4.speech.tts; + +import android.content.Context; +import android.os.Build; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.util.Log; + +/** Helper class for TTS functionality introduced in ICS */ +class TextToSpeechICS { + private static final String TAG = "android.support.v4.speech.tts"; + + static TextToSpeech construct(Context context, OnInitListener onInitListener, + String engineName) { + if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (engineName == null) { + return new TextToSpeech(context, onInitListener); + } else { + Log.w(TAG, "Can't specify tts engine on this device"); + return new TextToSpeech(context, onInitListener); + } + } else { + return new TextToSpeech(context, onInitListener, engineName); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java new file mode 100644 index 00000000000..bbad4c13ff5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java @@ -0,0 +1,74 @@ +package android.support.v4.speech.tts; + +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener; + +import java.util.Locale; +import java.util.Set; + +/** Helper class for TTS functionality introduced in ICS MR1 */ +class TextToSpeechICSMR1 { + /** + * Call {@link TextToSpeech#getFeatures} if available. + * + * @return {@link TextToSpeech#getFeatures} or null on older devices. + */ + static Set getFeatures(TextToSpeech tts, Locale locale) { + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + return tts.getFeatures(locale); + } + return null; + } + + public static final String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts"; + public static final String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts"; + + static interface UtteranceProgressListenerICSMR1 { + void onDone(String utteranceId); + void onError(String utteranceId); + void onStart(String utteranceId); + } + + /** + * Call {@link TextToSpeech#setOnUtteranceProgressListener} if ICS-MR1 or newer. + * + * On pre ICS-MR1 devices,{@link TextToSpeech#setOnUtteranceCompletedListener} is + * used to emulate its behavior - at the end of synthesis we call + * {@link UtteranceProgressListenerICSMR1#onStart(String)} and + * {@link UtteranceProgressListenerICSMR1#onDone(String)} one after the other. + * Errors can't be detected. + */ + static void setUtteranceProgressListener(TextToSpeech tts, + final UtteranceProgressListenerICSMR1 listener) { + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + tts.setOnUtteranceProgressListener(new UtteranceProgressListener() { + @Override + public void onStart(String utteranceId) { + listener.onStart(utteranceId); + } + + @Override + public void onError(String utteranceId) { + listener.onError(utteranceId); + } + + @Override + public void onDone(String utteranceId) { + listener.onDone(utteranceId); + } + }); + } else { + tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener() { + @Override + public void onUtteranceCompleted(String utteranceId) { + // Emulate onStart. Clients are expecting it will happen. + listener.onStart(utteranceId); + listener.onDone(utteranceId); + } + }); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java b/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java new file mode 100644 index 00000000000..4b9bc3ae364 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java @@ -0,0 +1,880 @@ +/* + * 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. + */ + +package android.support.v4.text; + +import android.support.v4.view.ViewCompat; + +import java.util.Locale; + +import static android.support.v4.text.TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR; + +/** + * Utility class for formatting text for display in a potentially opposite-directionality context + * without garbling. The directionality of the context is set at formatter creation and the + * directionality of the text can be either estimated or passed in when known. Provides the + * following functionality: + *

    + * 1. Bidi Wrapping + * When text in one language is mixed into a document in another, opposite-directionality language, + * e.g. when an English business name is embedded in a Hebrew web page, both the inserted string + * and the text surrounding it may be displayed incorrectly unless the inserted string is explicitly + * separated from the surrounding text in a "wrapper" that: + *

    + * - Declares its directionality so that the string is displayed correctly. This can be done in + * Unicode bidi formatting codes by {@link #unicodeWrap} and similar methods. + *

    + * - Isolates the string's directionality, so it does not unduly affect the surrounding content. + * Currently, this can only be done using invisible Unicode characters of the same direction as + * the context (LRM or RLM) in addition to the directionality declaration above, thus "resetting" + * the directionality to that of the context. The "reset" may need to be done at both ends of the + * string. Without "reset" after the string, the string will "stick" to a number or logically + * separate opposite-direction text that happens to follow it in-line (even if separated by + * neutral content like spaces and punctuation). Without "reset" before the string, the same can + * happen there, but only with more opposite-direction text, not a number. One approach is to + * "reset" the direction only after each string, on the theory that if the preceding opposite- + * direction text is itself bidi-wrapped, the "reset" after it will prevent the sticking. (Doing + * the "reset" only before each string definitely does not work because we do not want to require + * bidi-wrapping numbers, and a bidi-wrapped opposite-direction string could be followed by a + * number.) Still, the safest policy is to do the "reset" on both ends of each string, since RTL + * message translations often contain untranslated Latin-script brand names and technical terms, + * and one of these can be followed by a bidi-wrapped inserted value. On the other hand, when one + * has such a message, it is best to do the "reset" manually in the message translation itself, + * since the message's opposite-direction text could be followed by an inserted number, which we + * would not bidi-wrap anyway. Thus, "reset" only after the string is the current default. In an + * alternative to "reset", recent additions to the HTML, CSS, and Unicode standards allow the + * isolation to be part of the directionality declaration. This form of isolation is better than + * "reset" because it takes less space, does not require knowing the context directionality, has a + * gentler effect than "reset", and protects both ends of the string. However, we do not yet allow + * using it because required platforms do not yet support it. + *

    + * Providing these wrapping services is the basic purpose of the bidi formatter. + *

    + * 2. Directionality estimation + * How does one know whether a string about to be inserted into surrounding text has the same + * directionality? Well, in many cases, one knows that this must be the case when writing the code + * doing the insertion, e.g. when a localized message is inserted into a localized page. In such + * cases there is no need to involve the bidi formatter at all. In some other cases, it need not be + * the same as the context, but is either constant (e.g. urls are always LTR) or otherwise known. + * In the remaining cases, e.g. when the string is user-entered or comes from a database, the + * language of the string (and thus its directionality) is not known a priori, and must be + * estimated at run-time. The bidi formatter can do this automatically using the default + * first-strong estimation algorithm. It can also be configured to use a custom directionality + * estimation object. + */ +public final class BidiFormatter { + + /** + * The default text direction heuristic. + */ + private static TextDirectionHeuristicCompat DEFAULT_TEXT_DIRECTION_HEURISTIC = FIRSTSTRONG_LTR; + + /** + * Unicode "Left-To-Right Embedding" (LRE) character. + */ + private static final char LRE = '\u202A'; + + /** + * Unicode "Right-To-Left Embedding" (RLE) character. + */ + private static final char RLE = '\u202B'; + + /** + * Unicode "Pop Directional Formatting" (PDF) character. + */ + private static final char PDF = '\u202C'; + + /** + * Unicode "Left-To-Right Mark" (LRM) character. + */ + private static final char LRM = '\u200E'; + + /* + * Unicode "Right-To-Left Mark" (RLM) character. + */ + private static final char RLM = '\u200F'; + + /* + * String representation of LRM + */ + private static final String LRM_STRING = Character.toString(LRM); + + /* + * String representation of RLM + */ + private static final String RLM_STRING = Character.toString(RLM); + + /** + * Empty string constant. + */ + private static final String EMPTY_STRING = ""; + + /** + * A class for building a BidiFormatter with non-default options. + */ + public static final class Builder { + private boolean mIsRtlContext; + private int mFlags; + private TextDirectionHeuristicCompat mTextDirectionHeuristicCompat; + + /** + * Constructor. + * + */ + public Builder() { + initialize(isRtlLocale(Locale.getDefault())); + } + + /** + * Constructor. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public Builder(boolean rtlContext) { + initialize(rtlContext); + } + + /** + * Constructor. + * + * @param locale The context locale. + */ + public Builder(Locale locale) { + initialize(isRtlLocale(locale)); + } + + /** + * Initializes the builder with the given context directionality and default options. + * + * @param isRtlContext Whether the context is RTL or not. + */ + private void initialize(boolean isRtlContext) { + mIsRtlContext = isRtlContext; + mTextDirectionHeuristicCompat = DEFAULT_TEXT_DIRECTION_HEURISTIC; + mFlags = DEFAULT_FLAGS; + } + + /** + * Specifies whether the BidiFormatter to be built should also "reset" directionality before + * a string being bidi-wrapped, not just after it. The default is false. + */ + public Builder stereoReset(boolean stereoReset) { + if (stereoReset) { + mFlags |= FLAG_STEREO_RESET; + } else { + mFlags &= ~FLAG_STEREO_RESET; + } + return this; + } + + /** + * Specifies the default directionality estimation algorithm to be used by the BidiFormatter. + * By default, uses the first-strong heuristic. + * + * @param heuristic the {@code TextDirectionHeuristic} to use. + * @return the builder itself. + */ + public Builder setTextDirectionHeuristic(TextDirectionHeuristicCompat heuristic) { + mTextDirectionHeuristicCompat = heuristic; + return this; + } + + private static BidiFormatter getDefaultInstanceFromContext(boolean isRtlContext) { + return isRtlContext ? DEFAULT_RTL_INSTANCE : DEFAULT_LTR_INSTANCE; + } + + /** + * @return A BidiFormatter with the specified options. + */ + public BidiFormatter build() { + if (mFlags == DEFAULT_FLAGS && + mTextDirectionHeuristicCompat == DEFAULT_TEXT_DIRECTION_HEURISTIC) { + return getDefaultInstanceFromContext(mIsRtlContext); + } + return new BidiFormatter(mIsRtlContext, mFlags, mTextDirectionHeuristicCompat); + } + } + + // + private static final int FLAG_STEREO_RESET = 2; + private static final int DEFAULT_FLAGS = FLAG_STEREO_RESET; + + private static final BidiFormatter DEFAULT_LTR_INSTANCE = new BidiFormatter( + false /* LTR context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private static final BidiFormatter DEFAULT_RTL_INSTANCE = new BidiFormatter( + true /* RTL context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private final boolean mIsRtlContext; + private final int mFlags; + private final TextDirectionHeuristicCompat mDefaultTextDirectionHeuristicCompat; + + /** + * Factory for creating an instance of BidiFormatter for the default locale directionality. + * + */ + public static BidiFormatter getInstance() { + return new Builder().build(); + } + + /** + * Factory for creating an instance of BidiFormatter given the context directionality. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public static BidiFormatter getInstance(boolean rtlContext) { + return new Builder(rtlContext).build(); + } + + /** + * Factory for creating an instance of BidiFormatter given the context locale. + * + * @param locale The context locale. + */ + public static BidiFormatter getInstance(Locale locale) { + return new Builder(locale).build(); + } + + /** + * @param isRtlContext Whether the context directionality is RTL or not. + * @param flags The option flags. + * @param heuristic The default text direction heuristic. + */ + private BidiFormatter(boolean isRtlContext, int flags, TextDirectionHeuristicCompat heuristic) { + mIsRtlContext = isRtlContext; + mFlags = flags; + mDefaultTextDirectionHeuristicCompat = heuristic; + } + + /** + * @return Whether the context directionality is RTL + */ + public boolean isRtlContext() { + return mIsRtlContext; + } + + /** + * @return Whether directionality "reset" should also be done before a string being + * bidi-wrapped, not just after it. + */ + public boolean getStereoReset() { + return (mFlags & FLAG_STEREO_RESET) != 0; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the exit directionality of a given string is opposite to the context directionality. + * Putting this after the string (including its directionality declaration wrapping) prevents it + * from "sticking" to other opposite-directionality text or a number appearing after it inline + * with only neutral content in between. Otherwise returns the empty string. While the exit + * directionality is determined by scanning the end of the string, the overall directionality is + * given explicitly by a heuristic to estimate the {@code str}'s directionality. + * + * @param str String after which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + private String markAfter(String str, TextDirectionHeuristicCompat heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getExitDir() is called only if needed (short-circuit). + if (!mIsRtlContext && (isRtl || getExitDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (mIsRtlContext && (!isRtl || getExitDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the entry directionality of a given string is opposite to the context + * directionality. Putting this before the string (including its directionality declaration + * wrapping) prevents it from "sticking" to other opposite-directionality text appearing before + * it inline with only neutral content in between. Otherwise returns the empty string. While the + * entry directionality is determined by scanning the beginning of the string, the overall + * directionality is given explicitly by a heuristic to estimate the {@code str}'s directionality. + * + * @param str String before which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + private String markBefore(String str, TextDirectionHeuristicCompat heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getEntryDir() is called only if needed (short-circuit). + if (!mIsRtlContext && (isRtl || getEntryDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (mIsRtlContext && (!isRtl || getEntryDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Estimates the directionality of a string using the default text direction heuristic. + * + * @param str String whose directionality is to be estimated. + * @return true if {@code str}'s estimated overall directionality is RTL. Otherwise returns + * false. + */ + public boolean isRtl(String str) { + return mDefaultTextDirectionHeuristicCompat.isRtl(str, 0, str.length()); + } + + /** + * Formats a string of given directionality for use in plain-text output of the context + * directionality, so an opposite-directionality string is neither garbled nor garbles its + * surroundings. This makes use of Unicode bidi formatting characters. + *

    + * The algorithm: In case the given directionality doesn't match the context directionality, wraps + * the string with Unicode bidi formatting characters: RLE+{@code str}+PDF for RTL text, or + * LRE+{@code str}+PDF for LTR text. + *

    + * If {@code isolate}, directionally isolates the string so that it does not garble its + * surroundings. Currently, this is done by "resetting" the directionality after the string by + * appending a trailing Unicode bidi mark matching the context directionality (LRM or RLM) when + * either the overall directionality or the exit directionality of the string is opposite to that + * of the context. If the formatter was built using {@link Builder#stereoReset(boolean)} and + * passing "true" as an argument, also prepends a Unicode bidi mark matching the context + * directionality when either the overall directionality or the entry directionality of the + * string is opposite to that of the context. Note that as opposed to the overall + * directionality, the entry and exit directionalities are determined from the string itself. + *

    + * Does *not* do HTML-escaping. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristicCompat heuristic, boolean isolate) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + StringBuilder result = new StringBuilder(); + if (getStereoReset() && isolate) { + result.append(markBefore(str, + isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR)); + } + if (isRtl != mIsRtlContext) { + result.append(isRtl ? RLE : LRE); + result.append(str); + result.append(PDF); + } else { + result.append(str); + } + if (isolate) { + result.append(markAfter(str, + isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR)); + } + return result.toString(); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but assumes + * {@code isolate} is true. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristicCompat heuristic) { + return unicodeWrap(str, heuristic, true /* isolate */); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the + * formatter's default direction estimation algorithm. + * + * @param str The input string. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, boolean isolate) { + return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, isolate); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the + * formatter's default direction estimation algorithm and assumes {@code isolate} is true. + * + * @param str The input string. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str) { + return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, true /* isolate */); + } + + /** + * Helper method to return true if the Locale directionality is RTL. + * + * @param locale The Locale whose directionality will be checked to be RTL or LTR + * @return true if the {@code locale} directionality is RTL. False otherwise. + */ + private static boolean isRtlLocale(Locale locale) { + return (TextUtilsCompat.getLayoutDirectionFromLocale(locale) == ViewCompat.LAYOUT_DIRECTION_RTL); + } + + /** + * Enum for directionality type. + */ + private static final int DIR_LTR = -1; + private static final int DIR_UNKNOWN = 0; + private static final int DIR_RTL = +1; + + /** + * Returns the directionality of the last character with strong directionality in the string, or + * DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards from the end of + * the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its matching PDF as a + * strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results are undefined for a + * string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. The intended use is to check + * whether a logically separate item that starts with a number or a character of the string's + * exit directionality and follows this string inline (not counting any neutral characters in + * between) would "stick" to it in an opposite-directionality context, thus being displayed in + * an incorrect position. An LRM or RLM character (the one of the context's directionality) + * between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getExitDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getExitDir(); + } + + /** + * Returns the directionality of the first character with strong directionality in the string, + * or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL after + * RLE/RLO. The results are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF + * characters. The intended use is to check whether a logically separate item that ends with a + * character of the string's entry directionality and precedes the string inline (not counting + * any neutral characters in between) would "stick" to it in an opposite-directionality context, + * thus being displayed in an incorrect position. An LRM or RLM character (the one of the + * context's directionality) between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getEntryDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getEntryDir(); + } + + /** + * An object that estimates the directionality of a given string by various methods. + * + */ + private static class DirectionalityEstimator { + + // Internal static variables and constants. + + /** + * Size of the bidi character class cache. The results of the Character.getDirectionality() + * calls on the lowest DIR_TYPE_CACHE_SIZE codepoints are kept in an array for speed. + * The 0x700 value is designed to leave all the European and Near Eastern languages in the + * cache. It can be reduced to 0x180, restricting the cache to the Western European + * languages. + */ + private static final int DIR_TYPE_CACHE_SIZE = 0x700; + + /** + * The bidi character class cache. + */ + private static final byte DIR_TYPE_CACHE[]; + + static { + DIR_TYPE_CACHE = new byte[DIR_TYPE_CACHE_SIZE]; + for (int i = 0; i < DIR_TYPE_CACHE_SIZE; i++) { + DIR_TYPE_CACHE[i] = Character.getDirectionality(i); + } + } + + // Internal instance variables. + + /** + * The text to be scanned. + */ + private final String text; + + /** + * Whether the text to be scanned is to be treated as HTML, i.e. skipping over tags and + * entities when looking for the next / preceding dir type. + */ + private final boolean isHtml; + + /** + * The length of the text in chars. + */ + private final int length; + + /** + * The current position in the text. + */ + private int charIndex; + + /** + * The char encountered by the last dirTypeForward or dirTypeBackward call. If it + * encountered a supplementary codepoint, this contains a char that is not a valid + * codepoint. This is ok, because this member is only used to detect some well-known ASCII + * syntax, e.g. "http://" and the beginning of an HTML tag or entity. + */ + private char lastChar; + + /** + * Constructor. + * + * @param text The string to scan. + * @param isHtml Whether the text to be scanned is to be treated as HTML, i.e. skipping over + * tags and entities. + */ + DirectionalityEstimator(String text, boolean isHtml) { + this.text = text; + this.isHtml = isHtml; + length = text.length(); + } + + /** + * Returns the directionality of the first character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL + * after RLE/RLO. The results are undefined for a string containing unbalanced + * LRE/RLE/LRO/RLO/PDF characters. + */ + int getEntryDir() { + // The reason for this method name, as opposed to getFirstStrongDir(), is that + // "first strong" is a commonly used description of Unicode's estimation algorithm, + // but the two must treat formatting characters quite differently. Thus, we are staying + // away from both "first" and "last" in these method names to avoid confusion. + charIndex = 0; + int embeddingLevel = 0; + int embeddingLevelDir = DIR_UNKNOWN; + int firstNonEmptyEmbeddingLevel = 0; + while (charIndex < length && firstNonEmptyEmbeddingLevel == 0) { + switch (dirTypeForward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_LTR; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_RTL; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + --embeddingLevel; + // To restore embeddingLevelDir to its previous value, we would need a + // stack, which we want to avoid. Thus, at this point we do not know the + // current embedding's directionality. + embeddingLevelDir = DIR_UNKNOWN; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + default: + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + } + } + + // We have either found a non-empty embedding or scanned the entire string finding + // neither a non-empty embedding nor a strong character outside of an embedding. + if (firstNonEmptyEmbeddingLevel == 0) { + // We have not found a non-empty embedding. Thus, the string contains neither a + // non-empty embedding nor a strong character outside of an embedding. + return DIR_UNKNOWN; + } + + // We have found a non-empty embedding. + if (embeddingLevelDir != DIR_UNKNOWN) { + // We know the directionality of the non-empty embedding. + return embeddingLevelDir; + } + + // We do not remember the directionality of the non-empty embedding we found. So, we go + // backwards to find the start of the non-empty embedding and get its directionality. + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + } + } + // We should never get here. + return DIR_UNKNOWN; + } + + /** + * Returns the directionality of the last character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards + * from the end of the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its + * matching PDF as a strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results + * are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. + */ + int getExitDir() { + // The reason for this method name, as opposed to getLastStrongDir(), is that "last + // strong" sounds like the exact opposite of "first strong", which is a commonly used + // description of Unicode's estimation algorithm (getUnicodeDir() above), but the two + // must treat formatting characters quite differently. Thus, we are staying away from + // both "first" and "last" in these method names to avoid confusion. + charIndex = length; + int embeddingLevel = 0; + int lastNonEmptyEmbeddingLevel = 0; + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + default: + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + } + } + return DIR_UNKNOWN; + } + + // Internal methods + + /** + * Gets the bidi character class, i.e. Character.getDirectionality(), of a given char, using + * a cache for speed. Not designed for supplementary codepoints, whose results we do not + * cache. + */ + private static byte getCachedDirectionality(char c) { + return c < DIR_TYPE_CACHE_SIZE ? DIR_TYPE_CACHE[c] : Character.getDirectionality(c); + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the next codepoint and advances + * charIndex. If isHtml, and the codepoint is '<' or '&', advances through the tag/entity, + * and returns Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to + * figure out the actual character, and return its dirtype, but treating it as whitespace is + * good enough for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex >= length or < 0. + */ + byte dirTypeForward() { + lastChar = text.charAt(charIndex); + if (Character.isHighSurrogate(lastChar)) { + int codePoint = Character.codePointAt(text, charIndex); + charIndex += Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex++; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '<') { + dirType = skipTagForward(); + } else if (lastChar == '&') { + dirType = skipEntityForward(); + } + } + return dirType; + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the preceding codepoint and advances + * charIndex backwards. If isHtml, and the codepoint is the end of a complete HTML tag or + * entity, advances over the whole tag/entity and returns + * Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to figure out the + * actual character, and return its dirtype, but treating it as whitespace is good enough + * for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex > length or <= 0. + */ + byte dirTypeBackward() { + lastChar = text.charAt(charIndex - 1); + if (Character.isLowSurrogate(lastChar)) { + int codePoint = Character.codePointBefore(text, charIndex); + charIndex -= Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex--; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '>') { + dirType = skipTagBackward(); + } else if (lastChar == ';') { + dirType = skipEntityBackward(); + } + } + return dirType; + } + + /** + * Advances charIndex forward through an HTML tag (after the opening < has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching >, + * does not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the + * < that hadn't been part of a tag after all). + */ + private byte skipTagForward() { + int initialCharIndex = charIndex; + while (charIndex < length) { + lastChar = text.charAt(charIndex++); + if (lastChar == '>') { + // The end of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != quote) {} + } + } + // The original '<' wasn't the start of a tag after all. + charIndex = initialCharIndex; + lastChar = '<'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex backward through an HTML tag (after the closing > has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching <, does + * not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the > + * that hadn't been part of a tag after all). Nevertheless, the running time for calling + * skipTagBackward() in a loop remains linear in the size of the text, even for a text like + * ">>>>", because skipTagBackward() also stops looking for a matching < + * when it encounters another >. + */ + private byte skipTagBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '<') { + // The start of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '>') { + break; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex > 0 && (lastChar = text.charAt(--charIndex)) != quote) {} + } + } + // The original '>' wasn't the end of a tag after all. + charIndex = initialCharIndex; + lastChar = '>'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex forward through an HTML character entity tag (after the opening + * & has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be + * best to figure out the actual character and return its dirtype, but this is good enough. + */ + private byte skipEntityForward() { + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != ';') {} + return Character.DIRECTIONALITY_WHITESPACE; + } + + /** + * Advances charIndex backward through an HTML character entity tag (after the closing ; + * has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be best + * to figure out the actual character and return its dirtype, but this is good enough. + * If there is no matching &, does not change charIndex and returns + * Character.DIRECTIONALITY_OTHER_NEUTRALS (for the ';' that did not start an entity after + * all). Nevertheless, the running time for calling skipEntityBackward() in a loop remains + * linear in the size of the text, even for a text like ";;;;;;;", because skipTagBackward() + * also stops looking for a matching & when it encounters another ;. + */ + private byte skipEntityBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '&') { + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == ';') { + break; + } + } + charIndex = initialCharIndex; + lastChar = ';'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java new file mode 100644 index 00000000000..6df35ff7bba --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java @@ -0,0 +1,98 @@ +/* + * 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. + */ + +package android.support.v4.text; + +import android.os.Build; + +public class ICUCompat { + + interface ICUCompatImpl { + public String getScript(String locale); + public String addLikelySubtags(String locale); + } + + static class ICUCompatImplBase implements ICUCompatImpl { + @Override + public String getScript(String locale) { + return null; + } + + @Override + public String addLikelySubtags(String locale) { + return locale; + } + } + + static class ICUCompatImplIcs implements ICUCompatImpl { + @Override + public String getScript(String locale) { + return ICUCompatIcs.getScript(locale); + } + + @Override + public String addLikelySubtags(String locale) { + return ICUCompatIcs.addLikelySubtags(locale); + } + } + + private static final ICUCompatImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 14) { + IMPL = new ICUCompatImplIcs(); + } else { + IMPL = new ICUCompatImplBase(); + } + } + + /** + * Returns the script (language code) of a script. + * + * @param locale The locale. + * @return a String representing the script (language code) of the locale. + */ + public static String getScript(String locale) { + return IMPL.getScript(locale); + } + + /** + * Add the likely subtags for a provided locale ID, per the algorithm described in the following + * CLDR technical report: + * + * http://www.unicode.org/reports/tr35/#Likely_Subtags + * + * If locale is already in the maximal form, or there is no data available for maximization, + * it will be just returned. For example, "und-Zzzz" cannot be maximized, since there is no + * reasonable maximization. + * + * Examples: + * + * "en" maximizes to "en_Latn_US" + * "de" maximizes to "de_Latn_US" + * "sr" maximizes to "sr_Cyrl_RS" + * "sh" maximizes to "sr_Latn_RS" (Note this will not reverse.) + * "zh_Hani" maximizes to "zh_Hans_CN" (Note this will not reverse.) + + * @param locale The locale to maximize + * + * @return the maximized locale + */ + public static String addLikelySubtags(String locale) { + return IMPL.addLikelySubtags(locale); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java new file mode 100644 index 00000000000..7dc5d3c1b75 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package android.support.v4.text; + +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class ICUCompatIcs { + + private static final String TAG = "ICUCompatIcs"; + + private static Method sGetScriptMethod; + private static Method sAddLikelySubtagsMethod; + + static { + try { + final Class clazz = Class.forName("libcore.icu.ICU"); + if (clazz != null) { + sGetScriptMethod = clazz.getMethod("getScript", + new Class[]{ String.class }); + sAddLikelySubtagsMethod = clazz.getMethod("addLikelySubtags", + new Class[]{ String.class }); + } + } catch (Exception e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + } + + public static String getScript(String locale) { + try { + if (sGetScriptMethod != null) { + final Object[] args = new Object[] { locale }; + return (String) sGetScriptMethod.invoke(null, args); + } + } catch (IllegalAccessException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + catch (InvocationTargetException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + return null; + } + + public static String addLikelySubtags(String locale) { + try { + if (sAddLikelySubtagsMethod != null) { + final Object[] args = new Object[] { locale }; + return (String) sAddLikelySubtagsMethod.invoke(null, args); + } + } catch (IllegalAccessException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + catch (InvocationTargetException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + return locale; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java new file mode 100644 index 00000000000..ab9b2f70f1d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package android.support.v4.text; + +/** + * Interface for objects that use a heuristic for guessing at the paragraph direction by examining text. + */ +public interface TextDirectionHeuristicCompat { + /** + * Guess if a chars array is in the RTL direction or not. + * + * @param array the char array. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code array.length - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(char[] array, int start, int count); + + /** + * Guess if a {@code CharSequence} is in the RTL direction or not. + * + * @param cs the CharSequence. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code CharSequence.length() - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(CharSequence cs, int start, int count); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java new file mode 100644 index 00000000000..1fddfd1ee6e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java @@ -0,0 +1,257 @@ +/* + * 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. + */ + +package android.support.v4.text; + + +import android.support.v4.view.ViewCompat; + +import java.nio.CharBuffer; + +/** + * Some objects that implement TextDirectionHeuristic. + * + */ +public class TextDirectionHeuristicsCompat { + + /** + * Always decides that the direction is left to right. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat LTR = + new TextDirectionHeuristicInternal(null /* no algorithm */, false); + + /** + * Always decides that the direction is right to left. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat RTL = + new TextDirectionHeuristicInternal(null /* no algorithm */, true); + + /** + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to left to right if it finds none. This is the default behavior + * of the Unicode Bidirectional Algorithm. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat FIRSTSTRONG_LTR = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false); + + /** + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to right to left if it finds none. This is similar to the default + * behavior of the Unicode Bidirectional Algorithm, just with different fallback behavior. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat FIRSTSTRONG_RTL = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true); + + /** + * If the text contains any strong right to left non-format character, determines that the + * direction is right to left, falling back to left to right if it finds none. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat ANYRTL_LTR = + new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false); + + /** + * Force the paragraph direction to the Locale direction. Falls back to left to right. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat LOCALE = + TextDirectionHeuristicLocale.INSTANCE; + + /** + * State constants for taking care about true / false / unknown + */ + private static final int STATE_TRUE = 0; + private static final int STATE_FALSE = 1; + private static final int STATE_UNKNOWN = 2; + + private static int isRtlText(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } + } + + private static int isRtlTextOrFormat(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } + } + + /** + * Computes the text direction based on an algorithm. Subclasses implement + * {@link #defaultIsRtl} to handle cases where the algorithm cannot determine the + * direction from the text alone. + */ + private static abstract class TextDirectionHeuristicImpl implements TextDirectionHeuristicCompat { + private final TextDirectionAlgorithm mAlgorithm; + + public TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm) { + mAlgorithm = algorithm; + } + + /** + * Return true if the default text direction is rtl. + */ + abstract protected boolean defaultIsRtl(); + + @Override + public boolean isRtl(char[] array, int start, int count) { + return isRtl(CharBuffer.wrap(array), start, count); + } + + @Override + public boolean isRtl(CharSequence cs, int start, int count) { + if (cs == null || start < 0 || count < 0 || cs.length() - count < start) { + throw new IllegalArgumentException(); + } + if (mAlgorithm == null) { + return defaultIsRtl(); + } + return doCheck(cs, start, count); + } + + private boolean doCheck(CharSequence cs, int start, int count) { + switch(mAlgorithm.checkRtl(cs, start, count)) { + case STATE_TRUE: + return true; + case STATE_FALSE: + return false; + default: + return defaultIsRtl(); + } + } + } + + private static class TextDirectionHeuristicInternal extends TextDirectionHeuristicImpl { + private final boolean mDefaultIsRtl; + + private TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm, + boolean defaultIsRtl) { + super(algorithm); + mDefaultIsRtl = defaultIsRtl; + } + + @Override + protected boolean defaultIsRtl() { + return mDefaultIsRtl; + } + } + + /** + * Interface for an algorithm to guess the direction of a paragraph of text. + */ + private static interface TextDirectionAlgorithm { + /** + * Returns whether the range of text is RTL according to the algorithm. + */ + int checkRtl(CharSequence cs, int start, int count); + } + + /** + * Algorithm that uses the first strong directional character to determine the paragraph + * direction. This is the standard Unicode Bidirectional algorithm. + */ + private static class FirstStrong implements TextDirectionAlgorithm { + @Override + public int checkRtl(CharSequence cs, int start, int count) { + int result = STATE_UNKNOWN; + for (int i = start, e = start + count; i < e && result == STATE_UNKNOWN; ++i) { + result = isRtlTextOrFormat(Character.getDirectionality(cs.charAt(i))); + } + return result; + } + + private FirstStrong() { + } + + public static final FirstStrong INSTANCE = new FirstStrong(); + } + + /** + * Algorithm that uses the presence of any strong directional non-format + * character (e.g. excludes LRE, LRO, RLE, RLO) to determine the + * direction of text. + */ + private static class AnyStrong implements TextDirectionAlgorithm { + private final boolean mLookForRtl; + + @Override + public int checkRtl(CharSequence cs, int start, int count) { + boolean haveUnlookedFor = false; + for (int i = start, e = start + count; i < e; ++i) { + switch (isRtlText(Character.getDirectionality(cs.charAt(i)))) { + case STATE_TRUE: + if (mLookForRtl) { + return STATE_TRUE; + } + haveUnlookedFor = true; + break; + case STATE_FALSE: + if (!mLookForRtl) { + return STATE_FALSE; + } + haveUnlookedFor = true; + break; + default: + break; + } + } + if (haveUnlookedFor) { + return mLookForRtl ? STATE_FALSE : STATE_TRUE; + } + return STATE_UNKNOWN; + } + + private AnyStrong(boolean lookForRtl) { + this.mLookForRtl = lookForRtl; + } + + public static final AnyStrong INSTANCE_RTL = new AnyStrong(true); + public static final AnyStrong INSTANCE_LTR = new AnyStrong(false); + } + + /** + * Algorithm that uses the Locale direction to force the direction of a paragraph. + */ + private static class TextDirectionHeuristicLocale extends TextDirectionHeuristicImpl { + + public TextDirectionHeuristicLocale() { + super(null); + } + + @Override + protected boolean defaultIsRtl() { + final int dir = TextUtilsCompat.getLayoutDirectionFromLocale(java.util.Locale.getDefault()); + return (dir == ViewCompat.LAYOUT_DIRECTION_RTL); + } + + public static final TextDirectionHeuristicLocale INSTANCE = + new TextDirectionHeuristicLocale(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java new file mode 100644 index 00000000000..436d72fe0e5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java @@ -0,0 +1,118 @@ +/* + * 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. + */ + +package android.support.v4.text; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; + +import java.util.Locale; + +public class TextUtilsCompat { + + /** + * Html-encode the string. + * @param s the string to be encoded + * @return the encoded string + */ + @NonNull + public static String htmlEncode(@NonNull String s) { + StringBuilder sb = new StringBuilder(); + char c; + for (int i = 0; i < s.length(); i++) { + c = s.charAt(i); + switch (c) { + case '<': + sb.append("<"); //$NON-NLS-1$ + break; + case '>': + sb.append(">"); //$NON-NLS-1$ + break; + case '&': + sb.append("&"); //$NON-NLS-1$ + break; + case '\'': + //http://www.w3.org/TR/xhtml1 + // The named character reference ' (the apostrophe, U+0027) was introduced in + // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead + // of ' to work as expected in HTML 4 user agents. + sb.append("'"); //$NON-NLS-1$ + break; + case '"': + sb.append("""); //$NON-NLS-1$ + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Return the layout direction for a given Locale + * + * @param locale the Locale for which we want the layout direction. Can be null. + * @return the layout direction. This may be one of: + * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * Be careful: this code will need to be updated when vertical scripts will be supported + */ + public static int getLayoutDirectionFromLocale(@Nullable Locale locale) { + if (locale != null && !locale.equals(ROOT)) { + final String scriptSubtag = ICUCompat.getScript( + ICUCompat.addLikelySubtags(locale.toString())); + if (scriptSubtag == null) return getLayoutDirectionFromFirstChar(locale); + + if (scriptSubtag.equalsIgnoreCase(ARAB_SCRIPT_SUBTAG) || + scriptSubtag.equalsIgnoreCase(HEBR_SCRIPT_SUBTAG)) { + return ViewCompat.LAYOUT_DIRECTION_RTL; + } + } + + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + + /** + * Fallback algorithm to detect the locale direction. Rely on the fist char of the + * localized locale name. This will not work if the localized locale name is in English + * (this is the case for ICU 4.4 and "Urdu" script) + * + * @param locale + * @return the layout direction. This may be one of: + * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * Be careful: this code will need to be updated when vertical scripts will be supported + */ + private static int getLayoutDirectionFromFirstChar(Locale locale) { + switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) { + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return ViewCompat.LAYOUT_DIRECTION_RTL; + + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + default: + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + } + + public static final Locale ROOT = new Locale("", ""); + + private static String ARAB_SCRIPT_SUBTAG = "Arab"; + private static String HEBR_SCRIPT_SUBTAG = "Hebr"; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java b/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java new file mode 100644 index 00000000000..0a8a0ab5447 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java @@ -0,0 +1,205 @@ +/* + * 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. + */ + +package android.support.v4.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * ArrayMap is a generic key->value mapping data structure that is + * designed to be more memory efficient than a traditional {@link java.util.HashMap}, + * this implementation is a version of the platform's + * {@link android.util.ArrayMap} that can be used on older versions of the platform. + * It keeps its mappings in an array data structure -- an integer array of hash + * codes for each item, and an Object array of the key/value pairs. This allows it to + * avoid having to create an extra object for every entry put in to the map, and it + * also tries to control the growth of the size of these arrays more aggressively + * (since growing them only requires copying the entries in the array, not rebuilding + * a hash map). + * + *

    If you don't need the standard Java container APIs provided here (iterators etc), + * consider using {@link SimpleArrayMap} instead.

    + * + *

    Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

    + */ +public class ArrayMap extends SimpleArrayMap implements Map { + MapCollections mCollections; + + public ArrayMap() { + super(); + } + + /** + * Create a new ArrayMap with a given initial capacity. + */ + public ArrayMap(int capacity) { + super(capacity); + } + + /** + * Create a new ArrayMap with the mappings from the given ArrayMap. + */ + public ArrayMap(SimpleArrayMap map) { + super(map); + } + + private MapCollections getCollection() { + if (mCollections == null) { + mCollections = new MapCollections() { + @Override + protected int colGetSize() { + return mSize; + } + + @Override + protected Object colGetEntry(int index, int offset) { + return mArray[(index<<1) + offset]; + } + + @Override + protected int colIndexOfKey(Object key) { + return indexOfKey(key); + } + + @Override + protected int colIndexOfValue(Object value) { + return indexOfValue(value); + } + + @Override + protected Map colGetMap() { + return ArrayMap.this; + } + + @Override + protected void colPut(K key, V value) { + put(key, value); + } + + @Override + protected V colSetValue(int index, V value) { + return setValueAt(index, value); + } + + @Override + protected void colRemoveAt(int index) { + removeAt(index); + } + + @Override + protected void colClear() { + clear(); + } + }; + } + return mCollections; + } + + /** + * Determine if the array map contains all of the keys in the given collection. + * @param collection The collection whose contents are to be checked against. + * @return Returns true if this array map contains a key for every entry + * in collection, else returns false. + */ + public boolean containsAll(Collection collection) { + return MapCollections.containsAllHelper(this, collection); + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in map + * @param map The map whose contents are to be retrieved. + */ + @Override + public void putAll(Map map) { + ensureCapacity(mSize + map.size()); + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Remove all keys in the array map that exist in the given collection. + * @param collection The collection whose contents are to be used to remove keys. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean removeAll(Collection collection) { + return MapCollections.removeAllHelper(this, collection); + } + + /** + * Remove all keys in the array map that do not exist in the given collection. + * @param collection The collection whose contents are to be used to determine which + * keys to keep. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean retainAll(Collection collection) { + return MapCollections.retainAllHelper(this, collection); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all mappings + * in the array map. + * + *

    Note: this is a very inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + * + *

    Note:

    the semantics of this + * Set are subtly different than that of a {@link java.util.HashMap}: most important, + * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single + * object that exists for the entire iterator, so you can not hold on to it + * after calling {@link java.util.Iterator#next() Iterator.next}.

    + */ + @Override + public Set> entrySet() { + return getCollection().getEntrySet(); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all keys + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Set keySet() { + return getCollection().getKeySet(); + } + + /** + * Return a {@link java.util.Collection} for iterating over and interacting with all values + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Collection values() { + return getCollection().getValues(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java b/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java new file mode 100644 index 00000000000..69b255bbd55 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Static library support version of the framework's {@link android.util.AtomicFile}, + * a helper class for performing atomic operations on a file by creating a + * backup file until a write has successfully completed. + *

    + * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + *

    + * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + *

    + */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + /** + * Create a new AtomicFile for a file located at the given File path. + * The secondary backup file will be the same file path with ".bak" appended. + */ + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + /** + * Return the path to the base file. You should not generally use this, + * as the data at that path may not be valid. + */ + public File getBaseFile() { + return mBaseName; + } + + /** + * Delete the atomic file. This deletes both the base and backup files. + */ + public void delete() { + mBaseName.delete(); + mBackupName.delete(); + } + + /** + * Start a new write operation on the file. This returns a FileOutputStream + * to which you can write the new file data. The existing file is replaced + * with the new data. You must not directly close the given + * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} + * or {@link #failWrite(FileOutputStream)}. + * + *

    Note that if another thread is currently performing + * a write, this will simply replace whatever that thread is writing + * with the new file being written by this thread, and when the other + * thread finishes the write the new write operation will no longer be + * safe (or will be lost). You must do your own threading protection for + * access to AtomicFile. + */ + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBackupName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } else { + mBaseName.delete(); + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdir()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + /** + * Call when you have successfully finished writing to the stream + * returned by {@link #startWrite()}. This will close, sync, and + * commit the new data. The next attempt to read the atomic file + * will return the new file stream. + */ + public void finishWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + /** + * Call when you have failed for some reason at writing to the stream + * returned by {@link #startWrite()}. This will close the current + * write stream, and roll back to the previous state of the file. + */ + public void failWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + /** + * Open the atomic file for reading. If there previously was an + * incomplete write, this will roll back to the last good data before + * opening for read. You should call close() on the FileInputStream when + * you are done reading from it. + * + *

    Note that if another thread is currently performing + * a write, this will incorrectly consider it to be in the state of a bad + * write and roll back, causing the new data currently being written to + * be dropped. You must do your own threading protection for access to + * AtomicFile. + */ + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + /** + * A convenience for {@link #openRead()} that also reads all of the + * file contents into a byte array which is returned. + */ + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } + + static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java new file mode 100644 index 00000000000..760c9860e09 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java @@ -0,0 +1,252 @@ +/* + * 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. + */ +package android.support.v4.util; + +/** + * CircularArray is a generic circular array data structure that provides O(1) random read, O(1) + * prepend and O(1) append. The CircularArray automatically grows its capacity when number of added + * items is over its capacity. + */ +public final class CircularArray { + private E[] mElements; + private int mHead; + private int mTail; + private int mCapacityBitmask; + + private void doubleCapacity() { + int n = mElements.length; + int r = n - mHead; + int newCapacity = n << 1; + if (newCapacity < 0) { + throw new RuntimeException("Max array capacity exceeded"); + } + Object[] a = new Object[newCapacity]; + System.arraycopy(mElements, mHead, a, 0, r); + System.arraycopy(mElements, 0, a, r, mHead); + mElements = (E[]) a; + mHead = 0; + mTail = n; + mCapacityBitmask = newCapacity - 1; + } + + /** + * Create a CircularArray with default capacity. + */ + public CircularArray() { + this(8); + } + + /** + * Create a CircularArray with capacity for at least minCapacity elements. + * + * @param minCapacity The minimum capacity required for the CircularArray. + */ + public CircularArray(int minCapacity) { + if (minCapacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + int arrayCapacity = minCapacity; + // If minCapacity isn't a power of 2, round up to the next highest power + // of 2. + if (Integer.bitCount(minCapacity) != 1) { + arrayCapacity = 1 << (Integer.highestOneBit(minCapacity) + 1); + } + mCapacityBitmask = arrayCapacity - 1; + mElements = (E[]) new Object[arrayCapacity]; + } + + /** + * Add an element in front of the CircularArray. + * @param e Element to add. + */ + public void addFirst(E e) { + mHead = (mHead - 1) & mCapacityBitmask; + mElements[mHead] = e; + if (mHead == mTail) { + doubleCapacity(); + } + } + + /** + * Add an element at end of the CircularArray. + * @param e Element to add. + */ + public void addLast(E e) { + mElements[mTail] = e; + mTail = (mTail + 1) & mCapacityBitmask; + if (mTail == mHead) { + doubleCapacity(); + } + } + + /** + * Remove first element from front of the CircularArray and return it. + * @return The element removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E popFirst() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + E result = mElements[mHead]; + mElements[mHead] = null; + mHead = (mHead + 1) & mCapacityBitmask; + return result; + } + + /** + * Remove last element from end of the CircularArray and return it. + * @return The element removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E popLast() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + int t = (mTail - 1) & mCapacityBitmask; + E result = mElements[t]; + mElements[t] = null; + mTail = t; + return result; + } + + /** + * Remove all elements from the CircularArray. + */ + public void clear() { + removeFromStart(size()); + } + + /** + * Remove multiple elements from front of the CircularArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of elements to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromStart(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + int end = mElements.length; + if (numOfElements < end - mHead) { + end = mHead + numOfElements; + } + for (int i = mHead; i < end; i++) { + mElements[i] = null; + } + int removed = (end - mHead); + numOfElements -= removed; + mHead = (mHead + removed) & mCapacityBitmask; + if (numOfElements > 0) { + // mHead wrapped to 0 + for (int i = 0; i < numOfElements; i++) { + mElements[i] = null; + } + mHead = numOfElements; + } + } + + /** + * Remove multiple elements from end of the CircularArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of elements to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromEnd(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + int start = 0; + if (numOfElements < mTail) { + start = mTail - numOfElements; + } + for (int i = start; i < mTail; i++) { + mElements[i] = null; + } + int removed = (mTail - start); + numOfElements -= removed; + mTail = mTail - removed; + if (numOfElements > 0) { + // mTail wrapped to mElements.length + mTail = mElements.length; + int newTail = mTail - numOfElements; + for (int i = newTail; i < mTail; i++) { + mElements[i] = null; + } + mTail = newTail; + } + } + + /** + * Get first element of the CircularArray. + * @return The first element. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E getFirst() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[mHead]; + } + + /** + * Get last element of the CircularArray. + * @return The last element. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E getLast() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[(mTail - 1) & mCapacityBitmask]; + } + + /** + * Get nth (0 <= n <= size()-1) element of the CircularArray. + * @param n The zero based element index in the CircularArray. + * @return The nth element. + * @throws {@link ArrayIndexOutOfBoundsException} if n < 0 or n >= size(). + */ + public E get(int n) { + if (n < 0 || n >= size()) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[(mHead + n) & mCapacityBitmask]; + } + + /** + * Get number of elements in the CircularArray. + * @return Number of elements in the CircularArray. + */ + public int size() { + return (mTail - mHead) & mCapacityBitmask; + } + + /** + * Return true if size() is 0. + * @return true if size() is 0. + */ + public boolean isEmpty() { + return mHead == mTail; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java new file mode 100644 index 00000000000..23894368088 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java @@ -0,0 +1,207 @@ +/* + * 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. + */ +package android.support.v4.util; + +/** + * CircularIntArray is a circular integer array data structure that provides O(1) random read, O(1) + * prepend and O(1) append. The CircularIntArray automatically grows its capacity when number of + * added integers is over its capacity. + */ +public final class CircularIntArray +{ + private int[] mElements; + private int mHead; + private int mTail; + private int mCapacityBitmask; + + private void doubleCapacity() { + int n = mElements.length; + int r = n - mHead; + int newCapacity = n << 1; + if (newCapacity < 0) { + throw new RuntimeException("Max array capacity exceeded"); + } + int[] a = new int[newCapacity]; + System.arraycopy(mElements, mHead, a, 0, r); + System.arraycopy(mElements, 0, a, r, mHead); + mElements = a; + mHead = 0; + mTail = n; + mCapacityBitmask = newCapacity - 1; + } + + /** + * Create a CircularIntArray with default capacity. + */ + public CircularIntArray() { + this(8); + } + + /** + * Create a CircularIntArray with capacity for at least minCapacity elements. + * + * @param minCapacity The minimum capacity required for the CircularIntArray. + */ + public CircularIntArray(int minCapacity) { + if (minCapacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + int arrayCapacity = minCapacity; + // If minCapacity isn't a power of 2, round up to the next highest power + // of 2. + if (Integer.bitCount(minCapacity) != 1) { + arrayCapacity = 1 << (Integer.highestOneBit(minCapacity) + 1); + } + mCapacityBitmask = arrayCapacity - 1; + mElements = new int[arrayCapacity]; + } + + /** + * Add an integer in front of the CircularIntArray. + * @param e Integer to add. + */ + public void addFirst(int e) { + mHead = (mHead - 1) & mCapacityBitmask; + mElements[mHead] = e; + if (mHead == mTail) { + doubleCapacity(); + } + } + + /** + * Add an integer at end of the CircularIntArray. + * @param e Integer to add. + */ + public void addLast(int e) { + mElements[mTail] = e; + mTail = (mTail + 1) & mCapacityBitmask; + if (mTail == mHead) { + doubleCapacity(); + } + } + + /** + * Remove first integer from front of the CircularIntArray and return it. + * @return The integer removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int popFirst() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + int result = mElements[mHead]; + mHead = (mHead + 1) & mCapacityBitmask; + return result; + } + + /** + * Remove last integer from end of the CircularIntArray and return it. + * @return The integer removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int popLast() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + int t = (mTail - 1) & mCapacityBitmask; + int result = mElements[t]; + mTail = t; + return result; + } + + /** + * Remove all integers from the CircularIntArray. + */ + public void clear() { + mTail = mHead; + } + + /** + * Remove multiple integers from front of the CircularIntArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of integers to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromStart(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + mHead = (mHead + numOfElements) & mCapacityBitmask; + } + + /** + * Remove multiple elements from end of the CircularIntArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of integers to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromEnd(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + mTail = (mTail - numOfElements) & mCapacityBitmask; + } + + /** + * Get first integer of the CircularIntArray. + * @return The first integer. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int getFirst() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + return mElements[mHead]; + } + + /** + * Get last integer of the CircularIntArray. + * @return The last integer. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int getLast() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + return mElements[(mTail - 1) & mCapacityBitmask]; + } + + /** + * Get nth (0 <= n <= size()-1) integer of the CircularIntArray. + * @param n The zero based element index in the CircularIntArray. + * @return The nth integer. + * @throws {@link ArrayIndexOutOfBoundsException} if n < 0 or n >= size(). + */ + public int get(int n) { + if (n < 0 || n >= size()) throw new ArrayIndexOutOfBoundsException(); + return mElements[(mHead + n) & mCapacityBitmask]; + } + + /** + * Get number of integers in the CircularIntArray. + * @return Number of integers in the CircularIntArray. + */ + public int size() { + return (mTail - mHead) & mCapacityBitmask; + } + + /** + * Return true if size() is 0. + * @return true if size() is 0. + */ + public boolean isEmpty() { + return mHead == mTail; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java b/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java new file mode 100644 index 00000000000..74f39d33394 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package android.support.v4.util; + +class ContainerHelpers { + static final int[] EMPTY_INTS = new int[0]; + static final long[] EMPTY_LONGS = new long[0]; + static final Object[] EMPTY_OBJECTS = new Object[0]; + + public static int idealIntArraySize(int need) { + return idealByteArraySize(need * 4) / 4; + } + + public static int idealLongArraySize(int need) { + return idealByteArraySize(need * 8) / 8; + } + + public static int idealByteArraySize(int need) { + for (int i = 4; i < 32; i++) + if (need <= (1 << i) - 12) + return (1 << i) - 12; + + return need; + } + + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + // This is Arrays.binarySearch(), but doesn't do any argument validation. + static int binarySearch(int[] array, int size, int value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + int midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } + + static int binarySearch(long[] array, int size, long value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final long midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java b/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java new file mode 100644 index 00000000000..9d4f14ee71e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +/** + * Helper for accessing features in {@link android.util.DebugUtils} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class DebugUtils { + + public static void buildShortClassTag(Object cls, StringBuilder out) { + if (cls == null) { + out.append("null"); + } else { + String simpleName = cls.getClass().getSimpleName(); + if (simpleName == null || simpleName.length() <= 0) { + simpleName = cls.getClass().getName(); + int end = simpleName.lastIndexOf('.'); + if (end > 0) { + simpleName = simpleName.substring(end+1); + } + } + out.append(simpleName); + out.append('{'); + out.append(Integer.toHexString(System.identityHashCode(cls))); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java b/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java new file mode 100644 index 00000000000..53453e32270 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +import android.util.Log; + +import java.io.Writer; + +/** + * Helper for accessing features in {@link android.util.LogWriter} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class LogWriter extends Writer { + private final String mTag; + private StringBuilder mBuilder = new StringBuilder(128); + + /** + * Create a new Writer that sends to the log with the given priority + * and tag. + * + * @param tag A string tag to associate with each printed log statement. + */ + public LogWriter(String tag) { + mTag = tag; + } + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.d(mTag, mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java new file mode 100644 index 00000000000..25b6bb971e5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +/** + * SparseArray mapping longs to Objects, a version of the platform's + * {@link android.util.LongSparseArray} that can be used on older versions of the + * platform. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Longs to Objects, both because it avoids + * auto-boxing keys and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    To help with performance, the container includes an optimization when removing + * keys: instead of compacting its array immediately, it leaves the removed entry marked + * as deleted. The entry can then be re-used for the same key, or compacted later in + * a single garbage collection step of all removed entries. This garbage collection will + * need to be performed at any time the array needs to be grown or the the map size or + * entry values are retrieved.

    + */ +public class LongSparseArray implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private long[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new LongSparseArray containing no mappings. + */ + public LongSparseArray() { + this(10); + } + + /** + * Creates a new LongSparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongSparseArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = ContainerHelpers.EMPTY_LONGS; + mValues = ContainerHelpers.EMPTY_OBJECTS; + } else { + initialCapacity = ContainerHelpers.idealLongArraySize(initialCapacity); + mKeys = new long[initialCapacity]; + mValues = new Object[initialCapacity]; + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public LongSparseArray clone() { + LongSparseArray clone = null; + try { + clone = (LongSparseArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(long key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(long key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(long)}. + */ + public void remove(long key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + long[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ContainerHelpers.idealLongArraySize(mSize + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this LongSparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * LongSparseArray stores. + */ + public long keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * LongSparseArray stores. + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * LongSparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this LongSparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ContainerHelpers.idealLongArraySize(pos + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java b/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java new file mode 100644 index 00000000000..524e985ee7d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Static library version of {@link android.util.LruCache}. Used to write apps + * that run on API levels prior to 12. When running on API level 12 or above, + * this implementation is still used; it does not try to switch to the + * framework's implementation. See the framework SDK documentation for a class + * overview. + */ +public class LruCache { + private final LinkedHashMap map; + + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * + * @param maxSize The new maximum size. + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + public void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + *

    If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + *

    An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java b/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java new file mode 100644 index 00000000000..5ef54103736 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java @@ -0,0 +1,558 @@ +/* + * 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. + */ + +package android.support.v4.util; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Helper for writing standard Java collection interfaces to a data + * structure like {@link ArrayMap}. + * @hide + */ +abstract class MapCollections { + EntrySet mEntrySet; + KeySet mKeySet; + ValuesCollection mValues; + + final class ArrayIterator implements Iterator { + final int mOffset; + int mSize; + int mIndex; + boolean mCanRemove = false; + + ArrayIterator(int offset) { + mOffset = offset; + mSize = colGetSize(); + } + + @Override + public boolean hasNext() { + return mIndex < mSize; + } + + @Override + public T next() { + Object res = colGetEntry(mIndex, mOffset); + mIndex++; + mCanRemove = true; + return (T)res; + } + + @Override + public void remove() { + if (!mCanRemove) { + throw new IllegalStateException(); + } + mIndex--; + mSize--; + mCanRemove = false; + colRemoveAt(mIndex); + } + } + + final class MapIterator implements Iterator>, Map.Entry { + int mEnd; + int mIndex; + boolean mEntryValid = false; + + MapIterator() { + mEnd = colGetSize() - 1; + mIndex = -1; + } + + @Override + public boolean hasNext() { + return mIndex < mEnd; + } + + @Override + public Map.Entry next() { + mIndex++; + mEntryValid = true; + return this; + } + + @Override + public void remove() { + if (!mEntryValid) { + throw new IllegalStateException(); + } + colRemoveAt(mIndex); + mIndex--; + mEnd--; + mEntryValid = false; + } + + @Override + public K getKey() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (K)colGetEntry(mIndex, 0); + } + + @Override + public V getValue() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (V)colGetEntry(mIndex, 1); + } + + @Override + public V setValue(V object) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return colSetValue(mIndex, object); + } + + @Override + public final boolean equals(Object o) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return ContainerHelpers.equal(e.getKey(), colGetEntry(mIndex, 0)) + && ContainerHelpers.equal(e.getValue(), colGetEntry(mIndex, 1)); + } + + @Override + public final int hashCode() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + final Object key = colGetEntry(mIndex, 0); + final Object value = colGetEntry(mIndex, 1); + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); + } + + @Override + public final String toString() { + return getKey() + "=" + getValue(); + } + } + + final class EntrySet implements Set> { + @Override + public boolean add(Map.Entry object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> collection) { + int oldSize = colGetSize(); + for (Map.Entry entry : collection) { + colPut(entry.getKey(), entry.getValue()); + } + return oldSize != colGetSize(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + int index = colIndexOfKey(e.getKey()); + if (index < 0) { + return false; + } + Object foundVal = colGetEntry(index, 1); + return ContainerHelpers.equal(foundVal, e.getValue()); + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator> iterator() { + return new MapIterator(); + } + + @Override + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public T[] toArray(T[] array) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + final Object key = colGetEntry(i, 0); + final Object value = colGetEntry(i, 1); + result += ( (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()) ); + } + return result; + } + }; + + final class KeySet implements Set { + + @Override + public boolean add(K object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfKey(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + return containsAllHelper(colGetMap(), collection); + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(0); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfKey(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + return removeAllHelper(colGetMap(), collection); + } + + @Override + public boolean retainAll(Collection collection) { + return retainAllHelper(colGetMap(), collection); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + return toArrayHelper(0); + } + + @Override + public T[] toArray(T[] array) { + return toArrayHelper(array, 0); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + Object obj = colGetEntry(i, 0); + result += obj == null ? 0 : obj.hashCode(); + } + return result; + } + }; + + final class ValuesCollection implements Collection { + + @Override + public boolean add(V object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfValue(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(1); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfValue(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i T[] toArray(T[] array) { + return toArrayHelper(array, 1); + } + }; + + public static boolean containsAllHelper(Map map, Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!map.containsKey(it.next())) { + return false; + } + } + return true; + } + + public static boolean removeAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = collection.iterator(); + while (it.hasNext()) { + map.remove(it.next()); + } + return oldSize != map.size(); + } + + public static boolean retainAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + if (!collection.contains(it.next())) { + it.remove(); + } + } + return oldSize != map.size(); + } + + + public Object[] toArrayHelper(int offset) { + final int N = colGetSize(); + Object[] result = new Object[N]; + for (int i=0; i T[] toArrayHelper(T[] array, int offset) { + final int N = colGetSize(); + if (array.length < N) { + @SuppressWarnings("unchecked") T[] newArray + = (T[]) Array.newInstance(array.getClass().getComponentType(), N); + array = newArray; + } + for (int i=0; i N) { + array[N] = null; + } + return array; + } + + public static boolean equalsSetHelper(Set set, Object object) { + if (set == object) { + return true; + } + if (object instanceof Set) { + Set s = (Set) object; + + try { + return set.size() == s.size() && set.containsAll(s); + } catch (NullPointerException ignored) { + return false; + } catch (ClassCastException ignored) { + return false; + } + } + return false; + } + + public Set> getEntrySet() { + if (mEntrySet == null) { + mEntrySet = new EntrySet(); + } + return mEntrySet; + } + + public Set getKeySet() { + if (mKeySet == null) { + mKeySet = new KeySet(); + } + return mKeySet; + } + + public Collection getValues() { + if (mValues == null) { + mValues = new ValuesCollection(); + } + return mValues; + } + + protected abstract int colGetSize(); + protected abstract Object colGetEntry(int index, int offset); + protected abstract int colIndexOfKey(Object key); + protected abstract int colIndexOfValue(Object key); + protected abstract Map colGetMap(); + protected abstract void colPut(K key, V value); + protected abstract V colSetValue(int index, V value); + protected abstract void colRemoveAt(int index); + protected abstract void colClear(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java b/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java new file mode 100644 index 00000000000..7f9e61e92d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } + Pair p = (Pair) o; + return objectsEqual(p.first, first) && objectsEqual(p.second, second); + } + + private static boolean objectsEqual(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override + public int hashCode() { + return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java b/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java new file mode 100644 index 00000000000..e3907a1390c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java @@ -0,0 +1,166 @@ +/* + * 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. + */ + + +package android.support.v4.util; + + +/** + * Helper class for crating pools of objects. An example use looks like this: + *

    + * public class MyPooledClass {
    + *
    + *     private static final SynchronizedPool sPool =
    + *             new SynchronizedPool(10);
    + *
    + *     public static MyPooledClass obtain() {
    + *         MyPooledClass instance = sPool.acquire();
    + *         return (instance != null) ? instance : new MyPooledClass();
    + *     }
    + *
    + *     public void recycle() {
    + *          // Clear state if needed.
    + *          sPool.release(this);
    + *     }
    + *
    + *     . . .
    + * }
    + * 
    + * + */ +public final class Pools { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + public static interface Pool { + + /** + * @return An instance from the pool if such, null otherwise. + */ + public T acquire(); + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + public boolean release(T instance); + } + + private Pools() { + /* do nothing - hiding constructor */ + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SimplePool implements Pool { + private final Object[] mPool; + + private int mPoolSize; + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SimplePool(int maxPoolSize) { + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("The max pool size must be > 0"); + } + mPool = new Object[maxPoolSize]; + } + + @Override + @SuppressWarnings("unchecked") + public T acquire() { + if (mPoolSize > 0) { + final int lastPooledIndex = mPoolSize - 1; + T instance = (T) mPool[lastPooledIndex]; + mPool[lastPooledIndex] = null; + mPoolSize--; + return instance; + } + return null; + } + + @Override + public boolean release(T instance) { + if (isInPool(instance)) { + throw new IllegalStateException("Already in the pool!"); + } + if (mPoolSize < mPool.length) { + mPool[mPoolSize] = instance; + mPoolSize++; + return true; + } + return false; + } + + private boolean isInPool(T instance) { + for (int i = 0; i < mPoolSize; i++) { + if (mPool[i] == instance) { + return true; + } + } + return false; + } + } + + /** + * Synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SynchronizedPool extends SimplePool { + private final Object mLock = new Object(); + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SynchronizedPool(int maxPoolSize) { + super(maxPoolSize); + } + + @Override + public T acquire() { + synchronized (mLock) { + return super.acquire(); + } + } + + @Override + public boolean release(T element) { + synchronized (mLock) { + return super.release(element); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java b/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java new file mode 100644 index 00000000000..1b1577a6a08 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java @@ -0,0 +1,615 @@ +/* + * 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. + */ + +package android.support.v4.util; + +import android.util.Log; + +import java.util.Map; + +/** + * Base implementation of {@link ArrayMap} that doesn't include any standard Java + * container API interoperability. These features are generally heavier-weight ways + * to interact with the container, so discouraged, but they can be useful to make it + * easier to use as a drop-in replacement for HashMap. If you don't need them, this + * class can be preferrable since it doesn't bring in any of the implementation of those + * APIs, allowing that code to be stripped by ProGuard. + */ +public class SimpleArrayMap { + private static final boolean DEBUG = false; + private static final String TAG = "ArrayMap"; + + /** + * The minimum amount by which the capacity of a ArrayMap will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] mBaseCache; + static int mBaseCacheSize; + static Object[] mTwiceBaseCache; + static int mTwiceBaseCacheSize; + + int[] mHashes; + Object[] mArray; + int mSize; + + int indexOf(Object key, int hash) { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, hash); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index<<1])) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end << 1])) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i << 1])) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + int indexOfNull() { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, 0); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (null == mArray[index<<1]) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end << 1]) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i << 1]) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (size == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCache != null) { + final Object[] array = mTwiceBaseCache; + mArray = array; + mTwiceBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mTwiceBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes + + " now have " + mTwiceBaseCacheSize + " entries"); + return; + } + } + } else if (size == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCache != null) { + final Object[] array = mBaseCache; + mArray = array; + mBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes + + " now have " + mBaseCacheSize + " entries"); + return; + } + } + } + + mHashes = new int[size]; + mArray = new Object[size<<1]; + } + + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = mTwiceBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mTwiceBaseCache = array; + mTwiceBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 2x cache " + array + + " now have " + mTwiceBaseCacheSize + " entries"); + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCacheSize < CACHE_SIZE) { + array[0] = mBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mBaseCache = array; + mBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 1x cache " + array + + " now have " + mBaseCacheSize + " entries"); + } + } + } + } + + /** + * Create a new empty ArrayMap. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public SimpleArrayMap() { + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } + + /** + * Create a new ArrayMap with a given initial capacity. + */ + public SimpleArrayMap(int capacity) { + if (capacity == 0) { + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + } else { + allocArrays(capacity); + } + mSize = 0; + } + + /** + * Create a new ArrayMap with the mappings from the given ArrayMap. + */ + public SimpleArrayMap(SimpleArrayMap map) { + this(); + if (map != null) { + putAll(map); + } + } + + /** + * Make the array map empty. All storage is released. + */ + public void clear() { + if (mSize != 0) { + freeArrays(mHashes, mArray, mSize); + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } + } + + /** + * Ensure the array map can hold at least minimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize<<1); + } + freeArrays(ohashes, oarray, mSize); + } + } + + /** + * Check whether a key exists in the array. + * + * @param key The key to search for. + * @return Returns true if the key exists, else false. + */ + public boolean containsKey(Object key) { + return indexOfKey(key) >= 0; + } + + /** + * Returns the index of a key in the set. + * + * @param key The key to search for. + * @return Returns the index of the key if it exists, else a negative integer. + */ + public int indexOfKey(Object key) { + return key == null ? indexOfNull() : indexOf(key, key.hashCode()); + } + + int indexOfValue(Object value) { + final int N = mSize*2; + final Object[] array = mArray; + if (value == null) { + for (int i=1; i>1; + } + } + } else { + for (int i=1; i>1; + } + } + } + return -1; + } + + /** + * Check whether a value exists in the array. This requires a linear search + * through the entire array. + * + * @param value The value to search for. + * @return Returns true if the value exists, else false. + */ + public boolean containsValue(Object value) { + return indexOfValue(value) >= 0; + } + + /** + * Retrieve a value from the array. + * @param key The key of the value to retrieve. + * @return Returns the value associated with the given key, + * or null if there is no such key. + */ + public V get(Object key) { + final int index = indexOfKey(key); + return index >= 0 ? (V)mArray[(index<<1)+1] : null; + } + + /** + * Return the key at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the key stored at the given index. + */ + public K keyAt(int index) { + return (K)mArray[index << 1]; + } + + /** + * Return the value at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public V valueAt(int index) { + return (V)mArray[(index << 1) + 1]; + } + + /** + * Set the value at a given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @param value The new value to store at this index. + * @return Returns the previous value at the given index. + */ + public V setValueAt(int index, V value) { + index = (index << 1) + 1; + V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + /** + * Return true if the array map contains no items. + */ + public boolean isEmpty() { + return mSize <= 0; + } + + /** + * Add a new value to the array map. + * @param key The key under which to store the value. Must not be null. If + * this key already exists in the array, its value will be replaced. + * @param value The value to store for the given key. + * @return Returns the old value that was stored for the given key, or null if there + * was no such key. + */ + public V put(K key, V value) { + final int hash; + int index; + if (key == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = key.hashCode(); + index = indexOf(key, hash); + } + if (index >= 0) { + index = (index<<1) + 1; + final V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + index = ~index; + if (mSize >= mHashes.length) { + final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)) + : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); + + if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + if (mHashes.length > 0) { + if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + + freeArrays(ohashes, oarray, mSize); + } + + if (index < mSize) { + if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index) + + " to " + (index+1)); + System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index); + System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); + } + + mHashes[index] = hash; + mArray[index<<1] = key; + mArray[(index<<1)+1] = value; + mSize++; + return null; + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in array + * @param array The array whose contents are to be retrieved. + */ + public void putAll(SimpleArrayMap array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N<<1); + mSize = N; + } + } else { + for (int i=0; i= 0) { + return removeAt(index); + } + + return null; + } + + /** + * Remove the key/value mapping at the given index. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public V removeAt(int index) { + final Object old = mArray[(index << 1) + 1]; + if (mSize <= 1) { + // Now empty. + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + freeArrays(mHashes, mArray, mSize); + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } else { + if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { + // Shrunk enough to reduce size of arrays. We don't allow it to + // shrink smaller than (BASE_SIZE*2) to avoid flapping between + // that and BASE_SIZE. + final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2); + + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + mSize--; + if (index > 0) { + if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index << 1); + } + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + } else { + mSize--; + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + mArray[mSize << 1] = null; + mArray[(mSize << 1) + 1] = null; + } + } + return (V)old; + } + + /** + * Return the number of items in this array map. + */ + public int size() { + return mSize; + } + + /** + * {@inheritDoc} + * + *

    This implementation returns false if the object is not a map, or + * if the maps have different sizes. Otherwise, for each key in this map, + * values of both maps are compared. If the values for any key are not + * equal, the method returns false, otherwise it returns true. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof Map) { + Map map = (Map) object; + if (size() != map.size()) { + return false; + } + + try { + for (int i=0; iThis implementation composes a string by iterating over its mappings. If + * this map contains itself as a key or a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + Object key = keyAt(i); + if (key != this) { + buffer.append(key); + } else { + buffer.append("(this Map)"); + } + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java b/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java new file mode 100644 index 00000000000..aedc4ad08d8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +/** + * A copy of the current platform (currently {@link android.os.Build.VERSION_CODES#KITKAT} + * version of {@link android.util.SparseArray}; provides a removeAt() method and other things. + */ +public class SparseArrayCompat implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private int[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new SparseArray containing no mappings. + */ + public SparseArrayCompat() { + this(10); + } + + /** + * Creates a new SparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseArrayCompat(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = ContainerHelpers.EMPTY_INTS; + mValues = ContainerHelpers.EMPTY_OBJECTS; + } else { + initialCapacity = ContainerHelpers.idealIntArraySize(initialCapacity); + mKeys = new int[initialCapacity]; + mValues = new Object[initialCapacity]; + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public SparseArrayCompat clone() { + SparseArrayCompat clone = null; + try { + clone = (SparseArrayCompat) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(int key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(int key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(int)}. + */ + public void remove(int key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + /** + * Remove a range of mappings as a batch. + * + * @param index Index to begin at + * @param size Number of mappings to remove + */ + public void removeAtRange(int index, int size) { + final int end = Math.min(mSize, index + size); + for (int i = index; i < end; i++) { + removeAt(i); + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + int[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ContainerHelpers.idealIntArraySize(mSize + 1); + + int[] nkeys = new int[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseArray stores. + */ + public int keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseArray stores. + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * SparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + *

    Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + *

    Note also that unlike most collections' {@code indexOf} methods, + * this method compares values using {@code ==} rather than {@code equals}. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ContainerHelpers.idealIntArraySize(pos + 1); + + int[] nkeys = new int[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java b/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java new file mode 100644 index 00000000000..a57c94c9e21 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.util; + +import java.io.PrintWriter; + +/** + * Helper for accessing features in {@link android.util.TimeUtils} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class TimeUtils { + /** @hide Field length that can hold 999 days of time */ + public static final int HUNDRED_DAY_FIELD_LEN = 19; + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 60 * 60; + private static final int SECONDS_PER_DAY = 24 * 60 * 60; + + private static final Object sFormatSync = new Object(); + private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+5]; + + static private int accumField(int amt, int suffix, boolean always, int zeropad) { + if (amt > 99 || (always && zeropad >= 3)) { + return 3+suffix; + } + if (amt > 9 || (always && zeropad >= 2)) { + return 2+suffix; + } + if (always || amt > 0) { + return 1+suffix; + } + return 0; + } + + static private int printField(char[] formatStr, int amt, char suffix, int pos, + boolean always, int zeropad) { + if (always || amt > 0) { + final int startPos = pos; + if ((always && zeropad >= 3) || amt > 99) { + int dig = amt/100; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*100); + } + if ((always && zeropad >= 2) || amt > 9 || startPos != pos) { + int dig = amt/10; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*10); + } + formatStr[pos] = (char)(amt + '0'); + pos++; + formatStr[pos] = suffix; + pos++; + } + return pos; + } + + private static int formatDurationLocked(long duration, int fieldLen) { + if (sFormatStr.length < fieldLen) { + sFormatStr = new char[fieldLen]; + } + + char[] formatStr = sFormatStr; + + if (duration == 0) { + int pos = 0; + fieldLen -= 1; + while (pos < fieldLen) { + formatStr[pos] = ' '; + } + formatStr[pos] = '0'; + return pos+1; + } + + char prefix; + if (duration > 0) { + prefix = '+'; + } else { + prefix = '-'; + duration = -duration; + } + + int millis = (int)(duration%1000); + int seconds = (int) Math.floor(duration / 1000); + int days = 0, hours = 0, minutes = 0; + + if (seconds > SECONDS_PER_DAY) { + days = seconds / SECONDS_PER_DAY; + seconds -= days * SECONDS_PER_DAY; + } + if (seconds > SECONDS_PER_HOUR) { + hours = seconds / SECONDS_PER_HOUR; + seconds -= hours * SECONDS_PER_HOUR; + } + if (seconds > SECONDS_PER_MINUTE) { + minutes = seconds / SECONDS_PER_MINUTE; + seconds -= minutes * SECONDS_PER_MINUTE; + } + + int pos = 0; + + if (fieldLen != 0) { + int myLen = accumField(days, 1, false, 0); + myLen += accumField(hours, 1, myLen > 0, 2); + myLen += accumField(minutes, 1, myLen > 0, 2); + myLen += accumField(seconds, 1, myLen > 0, 2); + myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1; + while (myLen < fieldLen) { + formatStr[pos] = ' '; + pos++; + myLen++; + } + } + + formatStr[pos] = prefix; + pos++; + + int start = pos; + boolean zeropad = fieldLen != 0; + pos = printField(formatStr, days, 'd', pos, false, 0); + pos = printField(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0); + formatStr[pos] = 's'; + return pos + 1; + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, StringBuilder builder) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, 0); + builder.append(sFormatStr, 0, len); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw, int fieldLen) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, fieldLen); + pw.print(new String(sFormatStr, 0, len)); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw) { + formatDuration(duration, pw, 0); + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long time, long now, PrintWriter pw) { + if (time == 0) { + pw.print("--"); + return; + } + formatDuration(time-now, pw, 0); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/package.html b/mobile/google-android-support-v4/src/android/support/v4/util/package.html new file mode 100644 index 00000000000..afde9b750d7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/package.html @@ -0,0 +1,6 @@ + + +Support android.util classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java new file mode 100644 index 00000000000..0ea755ee672 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.os.Build; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing {@link View.AccessibilityDelegate} introduced after + * API level 4 in a backwards compatible fashion. + */ +public class AccessibilityDelegateCompat { + + static interface AccessibilityDelegateImpl { + public Object newAccessiblityDelegateDefaultImpl(); + public Object newAccessiblityDelegateBridge(AccessibilityDelegateCompat listener); + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info); + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(Object delegate, View host, int eventType); + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event); + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host); + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args); + } + + static class AccessibilityDelegateStubImpl implements AccessibilityDelegateImpl { + public Object newAccessiblityDelegateDefaultImpl() { + return null; + } + + @Override + public Object newAccessiblityDelegateBridge(AccessibilityDelegateCompat listener) { + return null; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return false; + } + + @Override + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info) { + + } + + @Override + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event) { + return true; + } + + @Override + public void sendAccessibilityEvent(Object delegate, View host, int eventType) { + + } + + @Override + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host) { + return null; + } + + @Override + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return false; + } + } + + static class AccessibilityDelegateIcsImpl extends AccessibilityDelegateStubImpl { + @Override + public Object newAccessiblityDelegateDefaultImpl() { + return AccessibilityDelegateCompatIcs.newAccessibilityDelegateDefaultImpl(); + } + + @Override + public Object newAccessiblityDelegateBridge(final AccessibilityDelegateCompat compat) { + return AccessibilityDelegateCompatIcs.newAccessibilityDelegateBridge( + new AccessibilityDelegateCompatIcs.AccessibilityDelegateBridge() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, + AccessibilityEvent event) { + return compat.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, Object info) { + compat.onInitializeAccessibilityNodeInfo(host, + new AccessibilityNodeInfoCompat(info)); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return compat.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + compat.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + compat.sendAccessibilityEventUnchecked(host, event); + } + }); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return AccessibilityDelegateCompatIcs.dispatchPopulateAccessibilityEvent(delegate, + host, event); + } + + @Override + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.onInitializeAccessibilityEvent(delegate, host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info) { + AccessibilityDelegateCompatIcs.onInitializeAccessibilityNodeInfo(delegate, host, + info.getInfo()); + } + + @Override + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.onPopulateAccessibilityEvent(delegate, host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event) { + return AccessibilityDelegateCompatIcs.onRequestSendAccessibilityEvent(delegate, host, + child, event); + } + + @Override + public void sendAccessibilityEvent(Object delegate, View host, int eventType) { + AccessibilityDelegateCompatIcs.sendAccessibilityEvent(delegate, host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.sendAccessibilityEventUnchecked(delegate, host, event); + } + } + + static class AccessibilityDelegateJellyBeanImpl extends AccessibilityDelegateIcsImpl { + @Override + public Object newAccessiblityDelegateBridge(final AccessibilityDelegateCompat compat) { + return AccessibilityDelegateCompatJellyBean.newAccessibilityDelegateBridge( + new AccessibilityDelegateCompatJellyBean + .AccessibilityDelegateBridgeJellyBean() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, + AccessibilityEvent event) { + return compat.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, Object info) { + compat.onInitializeAccessibilityNodeInfo(host, + new AccessibilityNodeInfoCompat(info)); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return compat.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + compat.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + compat.sendAccessibilityEventUnchecked(host, event); + } + + @Override + public Object getAccessibilityNodeProvider(View host) { + AccessibilityNodeProviderCompat provider = + compat.getAccessibilityNodeProvider(host); + return (provider != null) ? provider.getProvider() : null; + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return compat.performAccessibilityAction(host, action, args); + } + }); + } + + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host) { + Object provider = AccessibilityDelegateCompatJellyBean.getAccessibilityNodeProvider( + delegate, host); + if (provider != null) { + return new AccessibilityNodeProviderCompat(provider); + } + return null; + } + + @Override + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return AccessibilityDelegateCompatJellyBean.performAccessibilityAction(delegate, + host, action, args); + } + } + + private static final AccessibilityDelegateImpl IMPL; + private static final Object DEFAULT_DELEGATE; + + static { + if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityDelegateJellyBeanImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityDelegateIcsImpl(); + } else { + IMPL = new AccessibilityDelegateStubImpl(); + } + DEFAULT_DELEGATE = IMPL.newAccessiblityDelegateDefaultImpl(); + } + + final Object mBridge; + + /** + * Creates a new instance. + */ + public AccessibilityDelegateCompat() { + mBridge = IMPL.newAccessiblityDelegateBridge(this); + } + + /** + * @return The wrapped bridge implementation. + */ + Object getBridge() { + return mBridge; + } + + /** + * Sends an accessibility event of the given type. If accessibility is not + * enabled this method has no effect. + *

    + * The default implementation behaves as {@link View#sendAccessibilityEvent(int) + * View#sendAccessibilityEvent(int)} for the case of no accessibility delegate + * been set. + *

    + * + * @param host The View hosting the delegate. + * @param eventType The type of the event to send. + * + * @see View#sendAccessibilityEvent(int) View#sendAccessibilityEvent(int) + */ + public void sendAccessibilityEvent(View host, int eventType) { + IMPL.sendAccessibilityEvent(DEFAULT_DELEGATE, host, eventType); + } + + /** + * Sends an accessibility event. This method behaves exactly as + * {@link #sendAccessibilityEvent(View, int)} but takes as an argument an + * empty {@link AccessibilityEvent} and does not perform a check whether + * accessibility is enabled. + *

    + * The default implementation behaves as + * {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent) + * View#sendAccessibilityEventUnchecked(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event to send. + * + * @see View#sendAccessibilityEventUnchecked(AccessibilityEvent) + * View#sendAccessibilityEventUnchecked(AccessibilityEvent) + */ + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + IMPL.sendAccessibilityEventUnchecked(DEFAULT_DELEGATE, host, event); + } + + /** + * Dispatches an {@link AccessibilityEvent} to the host {@link View} first and then + * to its children for adding their text content to the event. + *

    + * The default implementation behaves as + * {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event. + * @return True if the event population was completed. + * + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return IMPL.dispatchPopulateAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Gives a chance to the host View to populate the accessibility event with its + * text content. + *

    + * The default implementation behaves as + * {@link ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent) + * ViewCompat#onPopulateAccessibilityEvent(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The accessibility event which to populate. + * + * @see ViewCompat#onPopulateAccessibilityEvent(View ,AccessibilityEvent) + * ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent) + */ + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + IMPL.onPopulateAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Initializes an {@link AccessibilityEvent} with information about the + * the host View which is the event source. + *

    + * The default implementation behaves as + * {@link ViewCompat#onInitializeAccessibilityEvent(View v, AccessibilityEvent event) + * ViewCompat#onInitalizeAccessibilityEvent(View v, AccessibilityEvent event)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event to initialize. + * + * @see ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent) + * ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + IMPL.onInitializeAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Initializes an {@link AccessibilityNodeInfoCompat} with information about the host view. + *

    + * The default implementation behaves as + * {@link ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + * ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param info The instance to initialize. + * + * @see ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + * ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + */ + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + IMPL.onInitializeAccessibilityNodeInfo(DEFAULT_DELEGATE, host, info); + } + + /** + * Called when a child of the host View has requested sending an + * {@link AccessibilityEvent} and gives an opportunity to the parent (the host) + * to augment the event. + *

    + * The default implementation behaves as + * {@link ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + * ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent + * + * @see ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + * ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + */ + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return IMPL.onRequestSendAccessibilityEvent(DEFAULT_DELEGATE, host, child, event); + } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + *

    + * The default implementation behaves as + * {@link ViewCompat#getAccessibilityNodeProvider(View) ViewCompat#getAccessibilityNodeProvider(View)} + * for the case of no accessibility delegate been set. + *

    + * + * @return The provider. + * + * @see AccessibilityNodeProviderCompat + */ + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { + return IMPL.getAccessibilityNodeProvider(DEFAULT_DELEGATE, host); + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfoCompat}. + *

    + * The default implementation behaves as + * {@link View#performAccessibilityAction(int, Bundle) + * View#performAccessibilityAction(int, Bundle)} for the case of + * no accessibility delegate been set. + *

    + * + * @param action The action to perform. + * @return Whether the action was performed. + * + * @see View#performAccessibilityAction(int, Bundle) + * View#performAccessibilityAction(int, Bundle) + */ + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return IMPL.performAccessibilityAction(DEFAULT_DELEGATE, host, action, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java new file mode 100644 index 00000000000..4adeb0cd0d4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * ICS specific AccessibilityDelegate API implementation. + */ +class AccessibilityDelegateCompatIcs { + + public interface AccessibilityDelegateBridge { + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View host, Object info); + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(View host, int eventType); + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event); + } + + public static Object newAccessibilityDelegateDefaultImpl() { + return new AccessibilityDelegate(); + } + + public static Object newAccessibilityDelegateBridge(final AccessibilityDelegateBridge bridge) { + return new AccessibilityDelegate() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return bridge.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + bridge.onInitializeAccessibilityNodeInfo(host, info); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return bridge.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + bridge.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + bridge.sendAccessibilityEventUnchecked(host, event); + } + }; + } + + public static boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return ((AccessibilityDelegate) delegate).dispatchPopulateAccessibilityEvent(host, event); + } + + public static void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).onInitializeAccessibilityEvent(host, event); + } + + public static void onInitializeAccessibilityNodeInfo(Object delegate, View host, Object info) { + ((AccessibilityDelegate) delegate).onInitializeAccessibilityNodeInfo(host, + (AccessibilityNodeInfo) info); + } + + public static void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).onPopulateAccessibilityEvent(host, event); + } + + public static boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, + View child, AccessibilityEvent event) { + return ((AccessibilityDelegate) delegate).onRequestSendAccessibilityEvent(host, child, + event); + } + + public static void sendAccessibilityEvent(Object delegate, View host, int eventType) { + ((AccessibilityDelegate) delegate).sendAccessibilityEvent(host, eventType); + } + + public static void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).sendAccessibilityEventUnchecked(host, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java new file mode 100644 index 00000000000..6651b624d48 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java @@ -0,0 +1,106 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.os.Bundle; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +/** + * JellyBean specific AccessibilityDelegate API implementation. + */ +class AccessibilityDelegateCompatJellyBean { + + public interface AccessibilityDelegateBridgeJellyBean { + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View host, Object info); + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(View host, int eventType); + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event); + public Object getAccessibilityNodeProvider(View host); + public boolean performAccessibilityAction(View host, int action, Bundle args); + } + + public static Object newAccessibilityDelegateBridge( + final AccessibilityDelegateBridgeJellyBean bridge) { + return new AccessibilityDelegate() { + + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return bridge.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + bridge.onInitializeAccessibilityNodeInfo(host, info); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return bridge.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + bridge.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + bridge.sendAccessibilityEventUnchecked(host, event); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + return (AccessibilityNodeProvider) bridge.getAccessibilityNodeProvider(host); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return bridge.performAccessibilityAction(host, action, args); + } + }; + } + + public static Object getAccessibilityNodeProvider(Object delegate, + View host) { + return ((AccessibilityDelegate) delegate).getAccessibilityNodeProvider(host); + } + + public static boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return ((AccessibilityDelegate) delegate).performAccessibilityAction(host, action, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java b/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java new file mode 100644 index 00000000000..f11876a89cd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.content.Context; +import android.util.Log; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.View; + +/** + * This class is a mediator for accomplishing a given task, for example sharing a file. It is + * responsible for creating a view that performs an action that accomplishes the task. This class + * also implements other functions such a performing a default action. + * + *

    Note: This class is included in the support library for compatibility + * with API level 4 and higher. If you're developing your app for API level 14 and higher + * only, you should instead use the framework {@link android.view.ActionProvider} + * class.

    + * + *

    An ActionProvider can be + * optionally specified for a {@link android.view.MenuItem} and in such a case it will be + * responsible for + * creating the action view that appears in the {@link android.app.ActionBar} as a substitute for + * the menu item when the item is displayed as an action item. Also the provider is responsible for + * performing a default action if a menu item placed on the overflow menu of the ActionBar is + * selected and none of the menu item callbacks has handled the selection. For this case the + * provider can also optionally provide a sub-menu for accomplishing the task at hand. + * + *

    There are two ways for using an action provider for creating and handling of action views: + * + *

    • Setting the action provider on a {@link android.view.MenuItem} directly by + * calling {@link + * android.support.v4.view.MenuItemCompat#setActionProvider(android.view.MenuItem, ActionProvider)}. + *
    • + * + *
    • Declaring the action provider in the menu XML resource. For example: + * + *
      
      + *   <item android:id="@+id/my_menu_item"
      + *     android:title="@string/my_menu_item_title"
      + *     android:icon="@drawable/my_menu_item_icon"
      + *     android:showAsAction="ifRoom"
      + *     android:actionProviderClass="foo.bar.SomeActionProvider" />
      + * 
      + *

    + * + * @see android.support.v4.view.MenuItemCompat#setActionProvider(android.view.MenuItem, ActionProvider) + * @see android.support.v4.view.MenuItemCompat#getActionProvider(android.view.MenuItem) + */ +public abstract class ActionProvider { + private static final String TAG = "ActionProvider(support)"; + private final Context mContext; + + private SubUiVisibilityListener mSubUiVisibilityListener; + private VisibilityListener mVisibilityListener; + + /** + * Creates a new instance. + * + * @param context Context for accessing resources. + */ + public ActionProvider(Context context) { + mContext = context; + } + + /** + * Gets the context associated with this action provider. + */ + public Context getContext() { + return mContext; + } + + /** + * Factory method for creating new action views. + * + * @return A new action view. + */ + public abstract View onCreateActionView(); + + /** + * Factory method called by the Android framework to create new action views. + * This method returns a new action view for the given MenuItem. + * + *

    If your ActionProvider implementation overrides the deprecated no-argument overload + * {@link #onCreateActionView()}, overriding this method for devices running API 16 or later + * is recommended but optional. The default implementation calls {@link #onCreateActionView()} + * for compatibility with applications written for older platform versions.

    + * + * @param forItem MenuItem to create the action view for + * @return the new action view + */ + public View onCreateActionView(MenuItem forItem) { + return onCreateActionView(); + } + + /** + * The result of this method determines whether or not {@link #isVisible()} will be used + * by the {@link MenuItem} this ActionProvider is bound to help determine its visibility. + * + * @return true if this ActionProvider overrides the visibility of the MenuItem + * it is bound to, false otherwise. The default implementation returns false. + * @see #isVisible() + */ + public boolean overridesItemVisibility() { + return false; + } + + /** + * If {@link #overridesItemVisibility()} returns true, the return value of this method + * will help determine the visibility of the {@link MenuItem} this ActionProvider is bound to. + * + *

    If the MenuItem's visibility is explicitly set to false by the application, + * the MenuItem will not be shown, even if this method returns true.

    + * + * @return true if the MenuItem this ActionProvider is bound to is visible, false if + * it is invisible. The default implementation returns true. + */ + public boolean isVisible() { + return true; + } + + /** + * If this ActionProvider is associated with an item in a menu, + * refresh the visibility of the item based on {@link #overridesItemVisibility()} and + * {@link #isVisible()}. If {@link #overridesItemVisibility()} returns false, this call + * will have no effect. + */ + public void refreshVisibility() { + if (mVisibilityListener != null && overridesItemVisibility()) { + mVisibilityListener.onActionProviderVisibilityChanged(isVisible()); + } + } + + /** + * Performs an optional default action. + * + *

    For the case of an action provider placed in a menu + * item not shown as an action this method is invoked if previous callbacks for processing menu + * selection has handled the event. + * + *

    A menu item selection is processed in the following order: + * + *

    • Receiving a call to + * {@link android.view.MenuItem.OnMenuItemClickListener#onMenuItemClick + * MenuItem.OnMenuItemClickListener.onMenuItemClick}.
    • + * + *
    • Receiving a call to + * {@link android.app.Activity#onOptionsItemSelected(android.view.MenuItem)} + * FragmentActivity.onOptionsItemSelected(MenuItem)} + *
    • + * + *
    • Receiving a call to + * {@link android.support.v4.app.Fragment#onOptionsItemSelected(android.view.MenuItem)} + * Fragment.onOptionsItemSelected(MenuItem)}
    • + * + *
    • Launching the {@link android.content.Intent} set via + * {@link android.view.MenuItem#setIntent(android.content.Intent) + * MenuItem.setIntent(android.content.Intent)} + *
    • + * + *
    • Invoking this method.
    + * + *

    The default implementation does not perform any action and returns false. + */ + public boolean onPerformDefaultAction() { + return false; + } + + /** + * Determines if this ActionProvider has a submenu associated with it. + * + *

    Associated submenus will be shown when an action view is not. This provider instance will + * receive a call to {@link #onPrepareSubMenu(SubMenu)} after the call to {@link + * #onPerformDefaultAction()} and before a submenu is displayed to the user. + * + * @return true if the item backed by this provider should have an associated submenu + */ + public boolean hasSubMenu() { + return false; + } + + /** + * Called to prepare an associated submenu for the menu item backed by this ActionProvider. + * + *

    if {@link #hasSubMenu()} returns true, this method will be called when the menu item is + * selected to prepare the submenu for presentation to the user. Apps may use this to create or + * alter submenu content right before display. + * + * @param subMenu Submenu that will be displayed + */ + public void onPrepareSubMenu(SubMenu subMenu) { + } + + /** + * Notify the system that the visibility of an action view's sub-UI such as an anchored popup + * has changed. This will affect how other system visibility notifications occur. + * + * @hide Pending future API approval + */ + public void subUiVisibilityChanged(boolean isVisible) { + if (mSubUiVisibilityListener != null) { + mSubUiVisibilityListener.onSubUiVisibilityChanged(isVisible); + } + } + + /** + * @hide Internal use only + */ + public void setSubUiVisibilityListener(SubUiVisibilityListener listener) { + mSubUiVisibilityListener = listener; + } + + /** + * Set a listener to be notified when this ActionProvider's overridden visibility changes. + * This should only be used by MenuItem implementations. + * + * @param listener listener to set + */ + public void setVisibilityListener(VisibilityListener listener) { + if (mVisibilityListener != null && listener != null) { + Log.w(TAG, "setVisibilityListener: Setting a new ActionProvider.VisibilityListener " + + "when one is already set. Are you reusing this " + getClass().getSimpleName() + + " instance while it is still in use somewhere else?"); + } + mVisibilityListener = listener; + } + + /** + * @hide Internal use only + */ + public interface SubUiVisibilityListener { + + public void onSubUiVisibilityChanged(boolean isVisible); + } + + /** + * Listens to changes in visibility as reported by {@link ActionProvider#refreshVisibility()}. + * + * @see ActionProvider#overridesItemVisibility() + * @see ActionProvider#isVisible() + */ + public interface VisibilityListener { + public void onActionProviderVisibilityChanged(boolean isVisible); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java new file mode 100644 index 00000000000..bcd1a0ac7da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java @@ -0,0 +1,564 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.view.GestureDetector; +import android.view.GestureDetector.OnDoubleTapListener; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Detects various gestures and events using the supplied {@link MotionEvent}s. + * The {@link OnGestureListener} callback will notify users when a particular + * motion event has occurred. This class should only be used with {@link MotionEvent}s + * reported via touch (don't use for trackball events). + * + *

    This compatibility implementation of the framework's GestureDetector guarantees + * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.

    + * + * To use this class: + *
      + *
    • Create an instance of the {@code GestureDetectorCompat} for your {@link View} + *
    • In the {@link View#onTouchEvent(MotionEvent)} method ensure you call + * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback + * will be executed when the events occur. + *
    + */ +public class GestureDetectorCompat { + interface GestureDetectorCompatImpl { + boolean isLongpressEnabled(); + boolean onTouchEvent(MotionEvent ev); + void setIsLongpressEnabled(boolean enabled); + void setOnDoubleTapListener(OnDoubleTapListener listener); + } + + static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl { + private int mTouchSlopSquare; + private int mDoubleTapSlopSquare; + private int mMinimumFlingVelocity; + private int mMaximumFlingVelocity; + + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + + // constants for Message.what used by GestureHandler below + private static final int SHOW_PRESS = 1; + private static final int LONG_PRESS = 2; + private static final int TAP = 3; + + private final Handler mHandler; + private final OnGestureListener mListener; + private OnDoubleTapListener mDoubleTapListener; + + private boolean mStillDown; + private boolean mDeferConfirmSingleTap; + private boolean mInLongPress; + private boolean mAlwaysInTapRegion; + private boolean mAlwaysInBiggerTapRegion; + + private MotionEvent mCurrentDownEvent; + private MotionEvent mPreviousUpEvent; + + /** + * True when the user is still touching for the second tap (down, move, and + * up events). Can only be true if there is a double tap listener attached. + */ + private boolean mIsDoubleTapping; + + private float mLastFocusX; + private float mLastFocusY; + private float mDownFocusX; + private float mDownFocusY; + + private boolean mIsLongpressEnabled; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + private class GestureHandler extends Handler { + GestureHandler() { + super(); + } + + GestureHandler(Handler handler) { + super(handler.getLooper()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW_PRESS: + mListener.onShowPress(mCurrentDownEvent); + break; + + case LONG_PRESS: + dispatchLongPress(); + break; + + case TAP: + // If the user's finger is still down, do not count it as a tap + if (mDoubleTapListener != null) { + if (!mStillDown) { + mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + } else { + mDeferConfirmSingleTap = true; + } + } + break; + + default: + throw new RuntimeException("Unknown message " + msg); //never + } + } + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use + * + * @throws NullPointerException if {@code listener} is null. + */ + public GestureDetectorCompatImplBase(Context context, OnGestureListener listener, + Handler handler) { + if (handler != null) { + mHandler = new GestureHandler(handler); + } else { + mHandler = new GestureHandler(); + } + mListener = listener; + if (listener instanceof OnDoubleTapListener) { + setOnDoubleTapListener((OnDoubleTapListener) listener); + } + init(context); + } + + private void init(Context context) { + if (context == null) { + throw new IllegalArgumentException("Context must not be null"); + } + if (mListener == null) { + throw new IllegalArgumentException("OnGestureListener must not be null"); + } + mIsLongpressEnabled = true; + + final ViewConfiguration configuration = ViewConfiguration.get(context); + final int touchSlop = configuration.getScaledTouchSlop(); + final int doubleTapSlop = configuration.getScaledDoubleTapSlop(); + mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); + + mTouchSlopSquare = touchSlop * touchSlop; + mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param onDoubleTapListener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. + */ + public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { + mDoubleTapListener = onDoubleTapListener; + } + + /** + * Set whether longpress is enabled, if this is enabled when a user + * presses and holds down you get a longpress event and nothing further. + * If it's disabled the user can press and hold down and then later + * moved their finger and you will get scroll events. By default + * longpress is enabled. + * + * @param isLongpressEnabled whether longpress should be enabled. + */ + public void setIsLongpressEnabled(boolean isLongpressEnabled) { + mIsLongpressEnabled = isLongpressEnabled; + } + + /** + * @return true if longpress is enabled, else false. + */ + public boolean isLongpressEnabled() { + return mIsLongpressEnabled; + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param ev The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final boolean pointerUp = + (action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP; + final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1; + + // Determine focal point + float sumX = 0, sumY = 0; + final int count = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < count; i++) { + if (skipIndex == i) continue; + sumX += MotionEventCompat.getX(ev, i); + sumY += MotionEventCompat.getY(ev, i); + } + final int div = pointerUp ? count - 1 : count; + final float focusX = sumX / div; + final float focusY = sumY / div; + + boolean handled = false; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEventCompat.ACTION_POINTER_DOWN: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + // Cancel long press and taps + cancelTaps(); + break; + + case MotionEventCompat.ACTION_POINTER_UP: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + + // Check the dot product of current velocities. + // If the pointer that left was opposing another velocity vector, clear. + mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final int upIndex = MotionEventCompat.getActionIndex(ev); + final int id1 = MotionEventCompat.getPointerId(ev, upIndex); + final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1); + final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1); + for (int i = 0; i < count; i++) { + if (i == upIndex) continue; + + final int id2 = MotionEventCompat.getPointerId(ev, i); + final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2); + final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2); + + final float dot = x + y; + if (dot < 0) { + mVelocityTracker.clear(); + break; + } + } + break; + + case MotionEvent.ACTION_DOWN: + if (mDoubleTapListener != null) { + boolean hadTapMessage = mHandler.hasMessages(TAP); + if (hadTapMessage) mHandler.removeMessages(TAP); + if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && + isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { + // This is a second tap + mIsDoubleTapping = true; + // Give a callback with the first tap of the double-tap + handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); + // Give a callback with down event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else { + // This is a first tap + mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + } + } + + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + if (mCurrentDownEvent != null) { + mCurrentDownEvent.recycle(); + } + mCurrentDownEvent = MotionEvent.obtain(ev); + mAlwaysInTapRegion = true; + mAlwaysInBiggerTapRegion = true; + mStillDown = true; + mInLongPress = false; + mDeferConfirmSingleTap = false; + + if (mIsLongpressEnabled) { + mHandler.removeMessages(LONG_PRESS); + mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() + + TAP_TIMEOUT + LONGPRESS_TIMEOUT); + } + mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); + handled |= mListener.onDown(ev); + break; + + case MotionEvent.ACTION_MOVE: + if (mInLongPress) { + break; + } + final float scrollX = mLastFocusX - focusX; + final float scrollY = mLastFocusY - focusY; + if (mIsDoubleTapping) { + // Give the move events of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mAlwaysInTapRegion) { + final int deltaX = (int) (focusX - mDownFocusX); + final int deltaY = (int) (focusY - mDownFocusY); + int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance > mTouchSlopSquare) { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastFocusX = focusX; + mLastFocusY = focusY; + mAlwaysInTapRegion = false; + mHandler.removeMessages(TAP); + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + } + if (distance > mTouchSlopSquare) { + mAlwaysInBiggerTapRegion = false; + } + } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastFocusX = focusX; + mLastFocusY = focusY; + } + break; + + case MotionEvent.ACTION_UP: + mStillDown = false; + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mIsDoubleTapping) { + // Finally, give the up event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mInLongPress) { + mHandler.removeMessages(TAP); + mInLongPress = false; + } else if (mAlwaysInTapRegion) { + handled = mListener.onSingleTapUp(ev); + if (mDeferConfirmSingleTap && mDoubleTapListener != null) { + mDoubleTapListener.onSingleTapConfirmed(ev); + } + } else { + // A fling must travel the minimum tap distance + final VelocityTracker velocityTracker = mVelocityTracker; + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final float velocityY = VelocityTrackerCompat.getYVelocity( + velocityTracker, pointerId); + final float velocityX = VelocityTrackerCompat.getXVelocity( + velocityTracker, pointerId); + + if ((Math.abs(velocityY) > mMinimumFlingVelocity) + || (Math.abs(velocityX) > mMinimumFlingVelocity)){ + handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); + } + } + if (mPreviousUpEvent != null) { + mPreviousUpEvent.recycle(); + } + // Hold the event we obtained above - listeners may have changed the original. + mPreviousUpEvent = currentUpEvent; + if (mVelocityTracker != null) { + // This may have been cleared when we called out to the + // application above. + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + mIsDoubleTapping = false; + mDeferConfirmSingleTap = false; + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + break; + + case MotionEvent.ACTION_CANCEL: + cancel(); + break; + } + + return handled; + } + + private void cancel() { + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); + mVelocityTracker.recycle(); + mVelocityTracker = null; + mIsDoubleTapping = false; + mStillDown = false; + mAlwaysInTapRegion = false; + mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; + if (mInLongPress) { + mInLongPress = false; + } + } + + private void cancelTaps() { + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); + mIsDoubleTapping = false; + mAlwaysInTapRegion = false; + mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; + if (mInLongPress) { + mInLongPress = false; + } + } + + private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, + MotionEvent secondDown) { + if (!mAlwaysInBiggerTapRegion) { + return false; + } + + if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { + return false; + } + + int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); + int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); + return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); + } + + private void dispatchLongPress() { + mHandler.removeMessages(TAP); + mDeferConfirmSingleTap = false; + mInLongPress = true; + mListener.onLongPress(mCurrentDownEvent); + } + } + + static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl { + private final GestureDetector mDetector; + + public GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener, + Handler handler) { + mDetector = new GestureDetector(context, listener, handler); + } + + @Override + public boolean isLongpressEnabled() { + return mDetector.isLongpressEnabled(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + @Override + public void setIsLongpressEnabled(boolean enabled) { + mDetector.setIsLongpressEnabled(enabled); + } + + @Override + public void setOnDoubleTapListener(OnDoubleTapListener listener) { + mDetector.setOnDoubleTapListener(listener); + } + } + + private final GestureDetectorCompatImpl mImpl; + + /** + * Creates a GestureDetectorCompat with the supplied listener. + * As usual, you may only use this constructor from a UI thread. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + */ + public GestureDetectorCompat(Context context, OnGestureListener listener) { + this(context, listener, null); + } + + /** + * Creates a GestureDetectorCompat with the supplied listener. + * As usual, you may only use this constructor from a UI thread. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler that will be used for posting deferred messages + */ + public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) { + if (Build.VERSION.SDK_INT > 17) { + mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler); + } else { + mImpl = new GestureDetectorCompatImplBase(context, listener, handler); + } + } + + /** + * @return true if longpress is enabled, else false. + */ + public boolean isLongpressEnabled() { + return mImpl.isLongpressEnabled(); + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param event The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent event) { + return mImpl.onTouchEvent(event); + } + + /** + * Set whether longpress is enabled, if this is enabled when a user + * presses and holds down you get a longpress event and nothing further. + * If it's disabled the user can press and hold down and then later + * moved their finger and you will get scroll events. By default + * longpress is enabled. + * + * @param enabled whether longpress should be enabled. + */ + public void setIsLongpressEnabled(boolean enabled) { + mImpl.setIsLongpressEnabled(enabled); + } + + /** + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param listener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. + */ + public void setOnDoubleTapListener(OnDoubleTapListener listener) { + mImpl.setOnDoubleTapListener(listener); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java new file mode 100644 index 00000000000..0e8558eb663 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java @@ -0,0 +1,203 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.graphics.Rect; +import android.os.Build; +import android.view.Gravity; + +/** + * Compatibility shim for accessing newer functionality from {@link android.view.Gravity}. + */ +public class GravityCompat { + interface GravityCompatImpl { + int getAbsoluteGravity(int gravity, int layoutDirection); + void apply(int gravity, int w, int h, Rect container, Rect outRect, int layoutDirection); + void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection); + void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection); + } + + static class GravityCompatImplBase implements GravityCompatImpl { + @Override + public int getAbsoluteGravity(int gravity, int layoutDirection) { + // Just strip off the relative bit to get LEFT/RIGHT. + return gravity & ~RELATIVE_LAYOUT_DIRECTION; + } + + @Override + public void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + Gravity.apply(gravity, w, h, container, outRect); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect); + } + + @Override + public void applyDisplay(int gravity, Rect display, Rect inoutObj, + int layoutDirection) { + Gravity.applyDisplay(gravity, display, inoutObj); + } + } + + static class GravityCompatImplJellybeanMr1 implements GravityCompatImpl { + @Override + public int getAbsoluteGravity(int gravity, int layoutDirection) { + return GravityCompatJellybeanMr1.getAbsoluteGravity(gravity, layoutDirection); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + GravityCompatJellybeanMr1.apply(gravity, w, h, container, outRect, layoutDirection); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + GravityCompatJellybeanMr1.apply(gravity, w, h, container, xAdj, yAdj, outRect, + layoutDirection); + } + + @Override + public void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + GravityCompatJellybeanMr1.applyDisplay(gravity, display, inoutObj, layoutDirection); + } + } + + static final GravityCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 17) { + IMPL = new GravityCompatImplJellybeanMr1(); + } else { + IMPL = new GravityCompatImplBase(); + } + } + + /** Raw bit controlling whether the layout direction is relative or not (START/END instead of + * absolute LEFT/RIGHT). + */ + public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000; + + /** Push object to x-axis position at the start of its container, not changing its size. */ + public static final int START = RELATIVE_LAYOUT_DIRECTION | Gravity.LEFT; + + /** Push object to x-axis position at the end of its container, not changing its size. */ + public static final int END = RELATIVE_LAYOUT_DIRECTION | Gravity.RIGHT; + + /** + * Binary mask for the horizontal gravity and script specific direction bit. + */ + public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END; + + /** + * Apply a gravity constant to an object and take care if layout direction is RTL or not. + * + * @param gravity The desired placement of the object, as defined by the + * constants in this class. + * @param w The horizontal size of the object. + * @param h The vertical size of the object. + * @param container The frame of the containing space, in which the object + * will be placed. Should be large enough to contain the + * width and height of the object. + * @param outRect Receives the computed frame of the object in its + * container. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void apply(int gravity, int w, int h, Rect container, + Rect outRect, int layoutDirection) { + IMPL.apply(gravity, w, h, container, outRect, layoutDirection); + } + + /** + * Apply a gravity constant to an object. + * + * @param gravity The desired placement of the object, as defined by the + * constants in this class. + * @param w The horizontal size of the object. + * @param h The vertical size of the object. + * @param container The frame of the containing space, in which the object + * will be placed. Should be large enough to contain the + * width and height of the object. + * @param xAdj Offset to apply to the X axis. If gravity is LEFT this + * pushes it to the right; if gravity is RIGHT it pushes it to + * the left; if gravity is CENTER_HORIZONTAL it pushes it to the + * right or left; otherwise it is ignored. + * @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes + * it down; if gravity is BOTTOM it pushes it up; if gravity is + * CENTER_VERTICAL it pushes it down or up; otherwise it is + * ignored. + * @param outRect Receives the computed frame of the object in its + * container. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void apply(int gravity, int w, int h, Rect container, + int xAdj, int yAdj, Rect outRect, int layoutDirection) { + IMPL.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection); + } + + /** + * Apply additional gravity behavior based on the overall "display" that an + * object exists in. This can be used after + * {@link android.view.Gravity#apply(int, int, int, Rect, int, int, Rect)} to place the object + * within a visible display. By default this moves or clips the object + * to be visible in the display; the gravity flags + * {@link android.view.Gravity#DISPLAY_CLIP_HORIZONTAL} and + * {@link android.view.Gravity#DISPLAY_CLIP_VERTICAL} can be used to change this behavior. + * + * @param gravity Gravity constants to modify the placement within the + * display. + * @param display The rectangle of the display in which the object is + * being placed. + * @param inoutObj Supplies the current object position; returns with it + * modified if needed to fit in the display. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + IMPL.applyDisplay(gravity, display, inoutObj, layoutDirection); + } + + /** + *

    Convert script specific gravity to absolute horizontal value.

    + * + * if horizontal direction is LTR, then START will set LEFT and END will set RIGHT. + * if horizontal direction is RTL, then START will set RIGHT and END will set LEFT. + * + * + * @param gravity The gravity to convert to absolute (horizontal) values. + * @param layoutDirection The layout direction. + * @return gravity converted to absolute (horizontal) values. + */ + public static int getAbsoluteGravity(int gravity, int layoutDirection) { + return IMPL.getAbsoluteGravity(gravity, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java new file mode 100644 index 00000000000..1e6077d5027 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.graphics.Rect; +import android.view.Gravity; + +class GravityCompatJellybeanMr1 { + + public static int getAbsoluteGravity(int gravity, int layoutDirection) { + return Gravity.getAbsoluteGravity(gravity, layoutDirection); + } + + public static void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + Gravity.apply(gravity, w, h, container, outRect, layoutDirection); + } + + public static void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection); + } + + public static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + Gravity.applyDisplay(gravity, display, inoutObj, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java new file mode 100644 index 00000000000..7a202cdd829 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java @@ -0,0 +1,211 @@ +/* + * 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.v4.view; + +/** + * Helper class for accessing values in {@link android.view.InputDevice}. + */ +public class InputDeviceCompat { + + /** + * A mask for input source classes. + * + * Each distinct input source constant has one or more input source class bits set to + * specify the desired interpretation for its input events. + */ + public static final int SOURCE_CLASS_MASK = 0x000000ff; + + /** + * The input source has no class. + * + * It is up to the application to determine how to handle the device based on the device type. + */ + public static final int SOURCE_CLASS_NONE = 0x00000000; + + /** + * The input source has buttons or keys. + * Examples: {@link #SOURCE_KEYBOARD}, {@link #SOURCE_DPAD}. + * + * A {@link android.view.KeyEvent} should be interpreted as a button or key press. + */ + public static final int SOURCE_CLASS_BUTTON = 0x00000001; + + /** + * The input source is a pointing device associated with a display. + * Examples: {@link #SOURCE_TOUCHSCREEN}, {@link #SOURCE_MOUSE}. + * + * A {@link android.view.MotionEvent} should be interpreted as absolute coordinates in + * display units according to the {@link android.view.View} hierarchy. Pointer down/up + * indicated when + * the finger touches the display or when the selection button is pressed/released. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of the pointing + * device. Some devices permit + * touches outside the display area so the effective range may be somewhat smaller or larger + * than the actual display size. + */ + public static final int SOURCE_CLASS_POINTER = 0x00000002; + + /** + * The input source is a trackball navigation device. + * Examples: {@link #SOURCE_TRACKBALL}. + * + * A {@link android.view.MotionEvent} should be interpreted as relative movements in + * device-specific + * units used for navigation purposes. Pointer down/up indicates when the selection button + * is pressed/released. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of motion. + */ + public static final int SOURCE_CLASS_TRACKBALL = 0x00000004; + + /** + * The input source is an absolute positioning device not associated with a display + * (unlike {@link #SOURCE_CLASS_POINTER}). + * + * A {@link android.view.MotionEvent} should be interpreted as absolute coordinates in + * device-specific surface units. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of positions. + */ + public static final int SOURCE_CLASS_POSITION = 0x00000008; + + /** + * The input source is a joystick. + * + * A {@link android.view.MotionEvent} should be interpreted as absolute joystick movements. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of positions. + */ + public static final int SOURCE_CLASS_JOYSTICK = 0x00000010; + + /** + * The input source is unknown. + */ + public static final int SOURCE_UNKNOWN = 0x00000000; + + /** + * The input source is a keyboard. + * + * This source indicates pretty much anything that has buttons. Use + * {@link android.view.InputDevice#getKeyboardType()} to determine whether the keyboard has + * alphabetic keys + * and can be used to enter text. + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_KEYBOARD = 0x00000100 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a DPad. + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_DPAD = 0x00000200 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a game pad. + * (It may also be a {@link #SOURCE_JOYSTICK}). + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_GAMEPAD = 0x00000400 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a touch screen pointing device. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_TOUCHSCREEN = 0x00001000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a mouse pointing device. + * This code is also used for other mouse-like pointing devices such as trackpads + * and trackpoints. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a stylus pointing device. + *

    + * Note that this bit merely indicates that an input device is capable of obtaining + * input from a stylus. To determine whether a given touch event was produced + * by a stylus, examine the tool type returned by {@link android.view.MotionEvent#getToolType(int)} + * for each individual pointer. + *

    + * A single touch event may multiple pointers with different tool types, + * such as an event that has one pointer with tool type + * {@link android.view.MotionEvent#TOOL_TYPE_FINGER} and another pointer with tool type + * {@link android.view.MotionEvent#TOOL_TYPE_STYLUS}. So it is important to examine + * the tool type of each pointer, regardless of the source reported + * by {@link android.view.MotionEvent#getSource()}. + *

    + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a trackball. + * + * @see #SOURCE_CLASS_TRACKBALL + */ + public static final int SOURCE_TRACKBALL = 0x00010000 | SOURCE_CLASS_TRACKBALL; + + /** + * The input source is a touch pad or digitizer tablet that is not + * associated with a display (unlike {@link #SOURCE_TOUCHSCREEN}). + * + * @see #SOURCE_CLASS_POSITION + */ + public static final int SOURCE_TOUCHPAD = 0x00100000 | SOURCE_CLASS_POSITION; + + /** + * The input source is a touch device whose motions should be interpreted as navigation events. + * + * For example, an upward swipe should be as an upward focus traversal in the same manner as + * pressing up on a D-Pad would be. Swipes to the left, right and down should be treated in a + * similar manner. + * + * @see #SOURCE_CLASS_NONE + */ + public static final int SOURCE_TOUCH_NAVIGATION = 0x00200000 | SOURCE_CLASS_NONE; + + /** + * The input source is a joystick. + * (It may also be a {@link #SOURCE_GAMEPAD}). + * + * @see #SOURCE_CLASS_JOYSTICK + */ + public static final int SOURCE_JOYSTICK = 0x01000000 | SOURCE_CLASS_JOYSTICK; + + /** + * The input source is a device connected through HDMI-based bus. + * + * The key comes in through HDMI-CEC or MHL signal line, and is treated as if it were + * generated by a locally connected DPAD or keyboard. + */ + public static final int SOURCE_HDMI = 0x02000000 | SOURCE_CLASS_BUTTON; + + /** + * A special input source constant that is used when filtering input devices + * to match devices that provide any type of input source. + */ + public static final int SOURCE_ANY = 0xffffff00; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java new file mode 100644 index 00000000000..fc0ee2e7a7b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.KeyEvent; +import android.view.View; + +/** + * Helper for accessing features in {@link KeyEvent} introduced after + * API level 4 in a backwards compatible fashion. + */ +public class KeyEventCompat { + /** + * Interface for the full API. + */ + interface KeyEventVersionImpl { + public int normalizeMetaState(int metaState); + public boolean metaStateHasModifiers(int metaState, int modifiers); + public boolean metaStateHasNoModifiers(int metaState); + public void startTracking(KeyEvent event); + public boolean isTracking(KeyEvent event); + public Object getKeyDispatcherState(View view); + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseKeyEventVersionImpl implements KeyEventVersionImpl { + private static final int META_MODIFIER_MASK = + KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON + | KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON + | KeyEvent.META_SYM_ON; + + // Mask of all lock key meta states. + private static final int META_ALL_MASK = META_MODIFIER_MASK; + + private static int metaStateFilterDirectionalModifiers(int metaState, + int modifiers, int basic, int left, int right) { + final boolean wantBasic = (modifiers & basic) != 0; + final int directional = left | right; + final boolean wantLeftOrRight = (modifiers & directional) != 0; + + if (wantBasic) { + if (wantLeftOrRight) { + throw new IllegalArgumentException("bad arguments"); + } + return metaState & ~directional; + } else if (wantLeftOrRight) { + return metaState & ~basic; + } else { + return metaState; + } + } + + @Override + public int normalizeMetaState(int metaState) { + if ((metaState & (KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON)) != 0) { + metaState |= KeyEvent.META_SHIFT_ON; + } + if ((metaState & (KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON)) != 0) { + metaState |= KeyEvent.META_ALT_ON; + } + return metaState & META_ALL_MASK; + } + + @Override + public boolean metaStateHasModifiers(int metaState, int modifiers) { + metaState = normalizeMetaState(metaState) & META_MODIFIER_MASK; + metaState = metaStateFilterDirectionalModifiers(metaState, modifiers, + KeyEvent.META_SHIFT_ON, KeyEvent.META_SHIFT_LEFT_ON, KeyEvent.META_SHIFT_RIGHT_ON); + metaState = metaStateFilterDirectionalModifiers(metaState, modifiers, + KeyEvent.META_ALT_ON, KeyEvent.META_ALT_LEFT_ON, KeyEvent.META_ALT_RIGHT_ON); + return metaState == modifiers; + } + + @Override + public boolean metaStateHasNoModifiers(int metaState) { + return (normalizeMetaState(metaState) & META_MODIFIER_MASK) == 0; + } + + @Override + public void startTracking(KeyEvent event) { + } + + @Override + public boolean isTracking(KeyEvent event) { + return false; + } + + @Override + public Object getKeyDispatcherState(View view) { + return null; + } + + @Override + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return event.dispatch(receiver); + } + } + + static class EclairKeyEventVersionImpl extends BaseKeyEventVersionImpl { + @Override + public void startTracking(KeyEvent event) { + KeyEventCompatEclair.startTracking(event); + } + + @Override + public boolean isTracking(KeyEvent event) { + return KeyEventCompatEclair.isTracking(event); + } + + @Override + public Object getKeyDispatcherState(View view) { + return KeyEventCompatEclair.getKeyDispatcherState(view); + } + + @Override + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return KeyEventCompatEclair.dispatch(event, receiver, state, target); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombKeyEventVersionImpl extends EclairKeyEventVersionImpl { + @Override + public int normalizeMetaState(int metaState) { + return KeyEventCompatHoneycomb.normalizeMetaState(metaState); + } + + @Override + public boolean metaStateHasModifiers(int metaState, int modifiers) { + return KeyEventCompatHoneycomb.metaStateHasModifiers(metaState, modifiers); + } + + @Override + public boolean metaStateHasNoModifiers(int metaState) { + return KeyEventCompatHoneycomb.metaStateHasNoModifiers(metaState); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final KeyEventVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombKeyEventVersionImpl(); + } else { + IMPL = new BaseKeyEventVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + public static int normalizeMetaState(int metaState) { + return IMPL.normalizeMetaState(metaState); + } + + public static boolean metaStateHasModifiers(int metaState, int modifiers) { + return IMPL.metaStateHasModifiers(metaState, modifiers); + } + + public static boolean metaStateHasNoModifiers(int metaState) { + return IMPL.metaStateHasNoModifiers(metaState); + } + + public static boolean hasModifiers(KeyEvent event, int modifiers) { + return IMPL.metaStateHasModifiers(event.getMetaState(), modifiers); + } + + public static boolean hasNoModifiers(KeyEvent event) { + return IMPL.metaStateHasNoModifiers(event.getMetaState()); + } + + public static void startTracking(KeyEvent event) { + IMPL.startTracking(event); + } + + public static boolean isTracking(KeyEvent event) { + return IMPL.isTracking(event); + } + + public static Object getKeyDispatcherState(View view) { + return IMPL.getKeyDispatcherState(view); + } + + public static boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return IMPL.dispatch(event, receiver, state, target); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java new file mode 100644 index 00000000000..b3b0d11afc3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.view.KeyEvent; +import android.view.View; + +class KeyEventCompatEclair { + public static Object getKeyDispatcherState(View view) { + return view.getKeyDispatcherState(); + } + + public static boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return event.dispatch(receiver, (KeyEvent.DispatcherState)state, target); + } + + public static void startTracking(KeyEvent event) { + event.startTracking(); + } + + public static boolean isTracking(KeyEvent event) { + return event.isTracking(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java new file mode 100644 index 00000000000..f1bd1179d65 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.KeyEvent; + +/** + * Implementation of key event compatibility that can call Honeycomb APIs. + */ +class KeyEventCompatHoneycomb { + public static int normalizeMetaState(int metaState) { + return KeyEvent.normalizeMetaState(metaState); + } + + public static boolean metaStateHasModifiers(int metaState, int modifiers) { + return KeyEvent.metaStateHasModifiers(metaState, modifiers); + } + + public static boolean metaStateHasNoModifiers(int metaState) { + return KeyEvent.metaStateHasNoModifiers(metaState); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java new file mode 100644 index 00000000000..d67a739bde1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.os.Build; +import android.view.LayoutInflater; + +/** + * Helper for accessing features in {@link android.view.LayoutInflater} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class LayoutInflaterCompat { + + interface LayoutInflaterCompatImpl { + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory); + } + + static class LayoutInflaterCompatImplBase implements LayoutInflaterCompatImpl { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatBase.setFactory(layoutInflater, factory); + } + } + + static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatHC.setFactory(layoutInflater, factory); + } + } + + static class LayoutInflaterCompatImplV21 extends LayoutInflaterCompatImplV11 { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatLollipop.setFactory(layoutInflater, factory); + } + } + + static final LayoutInflaterCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LayoutInflaterCompatImplV21(); + } else if (version >= 11) { + IMPL = new LayoutInflaterCompatImplV11(); + } else { + IMPL = new LayoutInflaterCompatImplBase(); + } + } + + /* + * Hide the constructor. + */ + private LayoutInflaterCompat() { + } + + /** + * Attach a custom Factory interface for creating views while using + * this LayoutInflater. This must not be null, and can only be set once; + * after setting, you can not change the factory. + * + * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory) + */ + public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + IMPL.setFactory(inflater, factory); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java new file mode 100644 index 00000000000..ea19333b2aa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +class LayoutInflaterCompatBase { + + static class FactoryWrapper implements LayoutInflater.Factory { + + final LayoutInflaterFactory mDelegateFactory; + + FactoryWrapper(LayoutInflaterFactory delegateFactory) { + mDelegateFactory = delegateFactory; + } + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + return mDelegateFactory.onCreateView(null, name, context, attrs); + } + + public String toString() { + return getClass().getName() + "{" + mDelegateFactory + "}"; + } + } + + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + inflater.setFactory(factory != null ? new FactoryWrapper(factory) : null); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java new file mode 100644 index 00000000000..06e72f49ce0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java @@ -0,0 +1,91 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +class LayoutInflaterCompatHC { + private static final String TAG = "LayoutInflaterCompatHC"; + + private static Field sLayoutInflaterFactory2Field; + private static boolean sCheckedField; + + static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapper + implements LayoutInflater.Factory2 { + + FactoryWrapperHC(LayoutInflaterFactory delegateFactory) { + super(delegateFactory); + } + + @Override + public View onCreateView(View parent, String name, Context context, + AttributeSet attributeSet) { + return mDelegateFactory.onCreateView(parent, name, context, attributeSet); + } + } + + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + final LayoutInflater.Factory2 factory2 = factory != null + ? new FactoryWrapperHC(factory) : null; + inflater.setFactory2(factory2); + + final LayoutInflater.Factory f = inflater.getFactory(); + if (f instanceof LayoutInflater.Factory2) { + // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21). + // We will now try and force set the merged factory to mFactory2 + forceSetFactory2(inflater, (LayoutInflater.Factory2) f); + } else { + // Else, we will force set the original wrapped Factory2 + forceSetFactory2(inflater, factory2); + } + } + + /** + * For APIs >= 11 && < 21, there was a framework bug that prevented a LayoutInflater's + * Factory2 from being merged properly if set after a cloneInContext from a LayoutInflater + * that already had a Factory2 registered. We work around that bug here. If we can't we + * log an error. + */ + static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) { + if (!sCheckedField) { + try { + sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2"); + sLayoutInflaterFactory2Field.setAccessible(true); + } catch (NoSuchFieldException e) { + Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class " + + LayoutInflater.class.getName() + + "; inflation may have unexpected results.", e); + } + sCheckedField = true; + } + if (sLayoutInflaterFactory2Field != null) { + try { + sLayoutInflaterFactory2Field.set(inflater, factory); + } catch (IllegalAccessException e) { + Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater " + + inflater + "; inflation may have unexpected results.", e); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java new file mode 100644 index 00000000000..b62c74e2b43 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java @@ -0,0 +1,27 @@ +/* + * 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.v4.view; + +import android.view.LayoutInflater; + +class LayoutInflaterCompatLollipop { + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + inflater.setFactory2(factory != null + ? new LayoutInflaterCompatHC.FactoryWrapperHC(factory) : null); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java new file mode 100644 index 00000000000..02b3d6360f7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +/** + * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as + * {@code LayoutInflater.Factory2}. + */ +public interface LayoutInflaterFactory { + + /** + * Hook you can supply that is called when inflating from a LayoutInflater. + * You can use this to customize the tag names available in your XML + * layout files. + * + * @param parent The parent that the created view will be placed + * in; note that this may be null. + * @param name Tag name to be inflated. + * @param context The context the view is being created in. + * @param attrs Inflation attributes as specified in XML file. + * + * @return View Newly created view. Return null for the default + * behavior. + */ + public View onCreateView(View parent, String name, Context context, AttributeSet attrs); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java new file mode 100644 index 00000000000..86072c04446 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java @@ -0,0 +1,233 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.os.Build; +import android.view.ViewGroup; + +/** + * Helper for accessing API features in + * {@link android.view.ViewGroup.MarginLayoutParams MarginLayoutParams} added after API 4. + */ +public class MarginLayoutParamsCompat { + interface MarginLayoutParamsCompatImpl { + int getMarginStart(ViewGroup.MarginLayoutParams lp); + int getMarginEnd(ViewGroup.MarginLayoutParams lp); + void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart); + void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd); + boolean isMarginRelative(ViewGroup.MarginLayoutParams lp); + int getLayoutDirection(ViewGroup.MarginLayoutParams lp); + void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection); + void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection); + } + + static class MarginLayoutParamsCompatImplBase implements MarginLayoutParamsCompatImpl { + + @Override + public int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return lp.leftMargin; + } + + @Override + public int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return lp.rightMargin; + } + + @Override + public void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + lp.leftMargin = marginStart; + } + + @Override + public void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + lp.rightMargin = marginEnd; + } + + @Override + public boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return false; + } + + @Override + public int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + + @Override + public void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + // No-op + } + + @Override + public void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + // No-op + } + } + + static class MarginLayoutParamsCompatImplJbMr1 implements MarginLayoutParamsCompatImpl { + + @Override + public int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getMarginStart(lp); + } + + @Override + public int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getMarginEnd(lp); + } + + @Override + public void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + MarginLayoutParamsCompatJellybeanMr1.setMarginStart(lp, marginStart); + } + + @Override + public void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + MarginLayoutParamsCompatJellybeanMr1.setMarginEnd(lp, marginEnd); + } + + @Override + public boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.isMarginRelative(lp); + } + + @Override + public int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getLayoutDirection(lp); + } + + @Override + public void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + MarginLayoutParamsCompatJellybeanMr1.setLayoutDirection(lp, layoutDirection); + } + + @Override + public void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + MarginLayoutParamsCompatJellybeanMr1.resolveLayoutDirection(lp, layoutDirection); + } + } + + static final MarginLayoutParamsCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 17) { // jb-mr1 + IMPL = new MarginLayoutParamsCompatImplJbMr1(); + } else { + IMPL = new MarginLayoutParamsCompatImplBase(); + } + } + + /** + * Get the relative starting margin that was set. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @return the margin along the starting edge in pixels + */ + public static int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return IMPL.getMarginStart(lp); + } + + /** + * Get the relative ending margin that was set. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @return the margin along the ending edge in pixels + */ + public static int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return IMPL.getMarginEnd(lp); + } + + /** + * Set the relative start margin. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @param marginStart the desired start margin in pixels + */ + public static void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + IMPL.setMarginStart(lp, marginStart); + } + + /** + * Set the relative end margin. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @param marginEnd the desired end margin in pixels + */ + public static void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + IMPL.setMarginEnd(lp, marginEnd); + } + + /** + * Check if margins are relative. + * + * @return true if either marginStart or marginEnd has been set. + */ + public static boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return IMPL.isMarginRelative(lp); + } + + /** + * Retuns the layout direction. Can be either {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * @return the layout direction. + */ + public static int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return IMPL.getLayoutDirection(lp); + } + + /** + * Set the layout direction. + * + * @param layoutDirection the layout direction. + * Should be either {@link ViewCompat#LAYOUT_DIRECTION_LTR} + * or {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + */ + public static void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + IMPL.setLayoutDirection(lp, layoutDirection); + } + + /** + * This will be called by {@link android.view.View#requestLayout()}. Left and Right margins + * may be overridden depending on layout direction. + */ + public static void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, + int layoutDirection) { + IMPL.resolveLayoutDirection(lp, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java new file mode 100644 index 00000000000..2fe9bc8d68e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class MarginLayoutParamsCompatJellybeanMr1 { + public static int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return lp.getMarginStart(); + } + + public static int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return lp.getMarginEnd(); + } + + public static void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + lp.setMarginStart(marginStart); + } + + public static void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + lp.setMarginEnd(marginEnd); + } + + public static boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return lp.isMarginRelative(); + } + + public static int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return lp.getLayoutDirection(); + } + + public static void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + lp.setLayoutDirection(layoutDirection); + } + + public static void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, + int layoutDirection) { + lp.resolveLayoutDirection(layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java new file mode 100644 index 00000000000..73c26ee204e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.MenuItem; + +/** + * Helper for accessing features in {@link android.view.Menu} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class MenuCompat { + /** + * Call {@link MenuItem#setShowAsAction(int) MenuItem.setShowAsAction()}. + * + * @deprecated Use {@link MenuItemCompat#setShowAsAction(MenuItem, int) + * MenuItemCompat.setShowAsAction(MenuItem, int)} + */ + @Deprecated + public static void setShowAsAction(MenuItem item, int actionEnum) { + MenuItemCompat.setShowAsAction(item, actionEnum); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java new file mode 100644 index 00000000000..8a52e9ec6eb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.support.v4.internal.view.SupportMenuItem; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; + +/** + * Helper for accessing features in {@link android.view.MenuItem} + * introduced after API level 4 in a backwards compatible fashion. + *

    Note: You cannot get an instance of this class. Instead, + * it provides static methods that correspond to the methods in {@link + * android.view.MenuItem}, but take a {@link android.view.MenuItem} object as an additional + * argument.

    + */ +public class MenuItemCompat { + private static final String TAG = "MenuItemCompat"; + + /** + * Never show this item as a button in an Action Bar. + */ + public static final int SHOW_AS_ACTION_NEVER = 0; + + /** + * Show this item as a button in an Action Bar if the system + * decides there is room for it. + */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + + /** + * Always show this item as a button in an Action Bar. Use sparingly! + * If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with + * smaller screens. A good rule of thumb is to have no more than 2 + * items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + + /** + * When this item is in the action bar, always show it with a + * text label even if it also has an icon specified. + */ + public static final int SHOW_AS_ACTION_WITH_TEXT = 4; + + /** + * This item's action view collapses to a normal menu item. + * When expanded, the action view temporarily takes over + * a larger segment of its container. + */ + public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; + + /** + * Interface for the full API. + */ + interface MenuVersionImpl { + void setShowAsAction(MenuItem item, int actionEnum); + MenuItem setActionView(MenuItem item, View view); + MenuItem setActionView(MenuItem item, int resId); + View getActionView(MenuItem item); + boolean expandActionView(MenuItem item); + boolean collapseActionView(MenuItem item); + boolean isActionViewExpanded(MenuItem item); + MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener); + } + + /** + * Interface definition for a callback to be invoked when a menu item marked with {@link + * #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} is expanded or collapsed. + * + * @see #expandActionView(android.view.MenuItem) + * @see #collapseActionView(android.view.MenuItem) + * @see #setShowAsAction(android.view.MenuItem, int) + */ + public interface OnActionExpandListener { + + /** + * Called when a menu item with {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is expanded. + * + * @param item Item that was expanded + * @return true if the item should expand, false if expansion should be suppressed. + */ + public boolean onMenuItemActionExpand(MenuItem item); + + /** + * Called when a menu item with {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is collapsed. + * + * @param item Item that was collapsed + * @return true if the item should collapse, false if collapsing should be suppressed. + */ + public boolean onMenuItemActionCollapse(MenuItem item); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseMenuVersionImpl implements MenuVersionImpl { + @Override + public void setShowAsAction(MenuItem item, int actionEnum) { + } + + @Override + public MenuItem setActionView(MenuItem item, View view) { + return item; + } + + @Override + public MenuItem setActionView(MenuItem item, int resId) { + return item; + } + + @Override + public View getActionView(MenuItem item) { + return null; + } + + @Override + public boolean expandActionView(MenuItem item) { + return false; + } + + @Override + public boolean collapseActionView(MenuItem item) { + return false; + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener) { + return item; + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombMenuVersionImpl implements MenuVersionImpl { + @Override + public void setShowAsAction(MenuItem item, int actionEnum) { + MenuItemCompatHoneycomb.setShowAsAction(item, actionEnum); + } + + @Override + public MenuItem setActionView(MenuItem item, View view) { + return MenuItemCompatHoneycomb.setActionView(item, view); + } + + @Override + public MenuItem setActionView(MenuItem item, int resId) { + return MenuItemCompatHoneycomb.setActionView(item, resId); + } + + @Override + public View getActionView(MenuItem item) { + return MenuItemCompatHoneycomb.getActionView(item); + } + + @Override + public boolean expandActionView(MenuItem item) { + return false; + } + + @Override + public boolean collapseActionView(MenuItem item) { + return false; + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener) { + return item; + } + } + + static class IcsMenuVersionImpl extends HoneycombMenuVersionImpl { + @Override + public boolean expandActionView(MenuItem item) { + return MenuItemCompatIcs.expandActionView(item); + } + + @Override + public boolean collapseActionView(MenuItem item) { + return MenuItemCompatIcs.collapseActionView(item); + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return MenuItemCompatIcs.isActionViewExpanded(item); + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, + final OnActionExpandListener listener) { + if (listener == null) { + return MenuItemCompatIcs.setOnActionExpandListener(item, null); + } + /* + * MenuItemCompatIcs is a dependency of this segment of the support lib + * but not the other way around, so we need to take an extra step here to proxy + * to the right types. + */ + return MenuItemCompatIcs.setOnActionExpandListener(item, + new MenuItemCompatIcs.SupportActionExpandProxy() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return listener.onMenuItemActionExpand(item); + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return listener.onMenuItemActionCollapse(item); + } + }); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final MenuVersionImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 14) { + IMPL = new IcsMenuVersionImpl(); + } else if (version >= 11) { + IMPL = new HoneycombMenuVersionImpl(); + } else { + IMPL = new BaseMenuVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Sets how this item should display in the presence of a compatible Action Bar. If the given + * item is compatible, this will call the item's supported implementation of + * {@link MenuItem#setShowAsAction(int)}. + * + * @param item - the item to change + * @param actionEnum - How the item should display. + */ + public static void setShowAsAction(MenuItem item, int actionEnum) { + if (item instanceof SupportMenuItem) { + ((SupportMenuItem) item).setShowAsAction(actionEnum); + } else { + IMPL.setShowAsAction(item, actionEnum); + } + } + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + * @param item the item to change + * @param view View to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * + * @see #setShowAsAction(MenuItem, int) + */ + public static MenuItem setActionView(MenuItem item, View view) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setActionView(view); + } + return IMPL.setActionView(item, view); + } + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + *

    + * Note: Setting an action view overrides the action provider + * set via {@link #setActionProvider(MenuItem, ActionProvider)}. + *

    + * + * @param item the item to change + * @param resId Layout resource to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * + * @see #setShowAsAction(MenuItem, int) + */ + public static MenuItem setActionView(MenuItem item, int resId) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setActionView(resId); + } + return IMPL.setActionView(item, resId); + } + + /** + * Returns the currently set action view for this menu item. + * + * @param item the item to query + * @return This item's action view + */ + public static View getActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).getActionView(); + } + return IMPL.getActionView(item); + } + + /** + * Sets the {@link ActionProvider} responsible for creating an action view if + * the item is placed on the action bar. The provider also provides a default + * action invoked if the item is placed in the overflow menu. + *

    + * Note: Setting an action provider overrides the action view + * set via {@link #setActionView(MenuItem, View)}. + *

    + * + * @param item item to change + * @param provider The action provider. + * @return This Item so additional setters can be called. + * + * @see ActionProvider + */ + public static MenuItem setActionProvider(MenuItem item, ActionProvider provider) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setSupportActionProvider(provider); + } + // TODO Wrap the support ActionProvider and assign it + Log.w(TAG, "setActionProvider: item does not implement SupportMenuItem; ignoring"); + return item; + } + + /** + * Gets the {@link ActionProvider}. + * + * @return The action provider. + * + * @see ActionProvider + * @see #setActionProvider(MenuItem, ActionProvider) + */ + public static ActionProvider getActionProvider(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).getSupportActionProvider(); + } + + // TODO Wrap the framework ActionProvider and return it + Log.w(TAG, "getActionProvider: item does not implement SupportMenuItem; returning null"); + return null; + } + + /** + * Expand the action view associated with this menu item. + * The menu item must have an action view set, as well as + * the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * If a listener has been set using + * {@link #setOnActionExpandListener(MenuItem, OnActionExpandListener)} + * it will have its {@link OnActionExpandListener#onMenuItemActionExpand(MenuItem)} + * method invoked. The listener may return false from this method to prevent expanding + * the action view. + * + * @return true if the action view was expanded, false otherwise. + */ + public static boolean expandActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).expandActionView(); + } + return IMPL.expandActionView(item); + } + + /** + * Collapse the action view associated with this menu item. The menu item must have an action + * view set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setOnActionExpandListener(MenuItem, + * android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link + * android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} + * method invoked. The listener may return false from this method to prevent collapsing + * the action view. + * + * @return true if the action view was collapsed, false otherwise. + */ + public static boolean collapseActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).collapseActionView(); + } + return IMPL.collapseActionView(item); + } + + /** + * Returns true if this menu item's action view has been expanded. + * + * @return true if the item's action view is expanded, false otherwise. + * @see #expandActionView(MenuItem) + * @see #collapseActionView(MenuItem) + * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @see android.support.v4.view.MenuItemCompat.OnActionExpandListener + */ + public static boolean isActionViewExpanded(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).isActionViewExpanded(); + } + return IMPL.isActionViewExpanded(item); + } + + /** + * Set an {@link OnActionExpandListener} on this menu + * item to be notified when the associated action view is expanded or collapsed. + * The menu item must be configured to expand or collapse its action view using the flag + * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * + * @param listener Listener that will respond to expand/collapse events + * @return This menu item instance for call chaining + */ + public static MenuItem setOnActionExpandListener(MenuItem item, + OnActionExpandListener listener) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setSupportOnActionExpandListener(listener); + } + return IMPL.setOnActionExpandListener(item, listener); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java new file mode 100644 index 00000000000..1a0e513ee79 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.MenuItem; +import android.view.View; + +/** + * Implementation of menu compatibility that can call Honeycomb APIs. + */ +class MenuItemCompatHoneycomb { + public static void setShowAsAction(MenuItem item, int actionEnum) { + item.setShowAsAction(actionEnum); + } + + public static MenuItem setActionView(MenuItem item, View view) { + return item.setActionView(view); + } + + public static MenuItem setActionView(MenuItem item, int resId) { + return item.setActionView(resId); + } + + public static View getActionView(MenuItem item) { + return item.getActionView(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java new file mode 100644 index 00000000000..8e65079d012 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java @@ -0,0 +1,68 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.view.MenuItem; + +class MenuItemCompatIcs { + public static boolean expandActionView(MenuItem item) { + return item.expandActionView(); + } + + public static boolean collapseActionView(MenuItem item) { + return item.collapseActionView(); + } + + public static boolean isActionViewExpanded(MenuItem item) { + return item.isActionViewExpanded(); + } + + public static MenuItem setOnActionExpandListener(MenuItem item, + SupportActionExpandProxy listener) { + return item.setOnActionExpandListener(new OnActionExpandListenerWrapper(listener)); + } + + /** + * Work around the support lib's build dependency chain. The actual API-lib + * depends on -ics, but -ics doesn't depend on the API-lib so it doesn't know + * that MenuItemCompat.OnActionExpandListener exists. + */ + interface SupportActionExpandProxy { + boolean onMenuItemActionExpand(MenuItem item); + boolean onMenuItemActionCollapse(MenuItem item); + } + + // support => framework + static class OnActionExpandListenerWrapper implements MenuItem.OnActionExpandListener { + private SupportActionExpandProxy mWrapped; + + public OnActionExpandListenerWrapper(SupportActionExpandProxy wrapped) { + mWrapped = wrapped; + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return mWrapped.onMenuItemActionExpand(item); + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return mWrapped.onMenuItemActionCollapse(item); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java new file mode 100644 index 00000000000..8c7d07dc6e7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.os.Build; +import android.view.MotionEvent; + +/** + * Helper for accessing features in {@link MotionEvent} introduced + * after API level 4 in a backwards compatible fashion. + */ +public class MotionEventCompat { + /** + * Interface for the full API. + */ + interface MotionEventVersionImpl { + public int findPointerIndex(MotionEvent event, int pointerId); + public int getPointerId(MotionEvent event, int pointerIndex); + public float getX(MotionEvent event, int pointerIndex); + public float getY(MotionEvent event, int pointerIndex); + public int getPointerCount(MotionEvent event); + public int getSource(MotionEvent event); + float getAxisValue(MotionEvent event, int axis); + float getAxisValue(MotionEvent event, int axis, int pointerIndex); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseMotionEventVersionImpl implements MotionEventVersionImpl { + @Override + public int findPointerIndex(MotionEvent event, int pointerId) { + if (pointerId == 0) { + // id 0 == index 0 and vice versa. + return 0; + } + return -1; + } + @Override + public int getPointerId(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + // index 0 == id 0 and vice versa. + return 0; + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public float getX(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + return event.getX(); + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public float getY(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + return event.getY(); + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public int getPointerCount(MotionEvent event) { + return 1; + } + + @Override + public int getSource(MotionEvent event) { + return InputDeviceCompat.SOURCE_UNKNOWN; + } + + @Override + public float getAxisValue(MotionEvent event, int axis) { + return 0; + } + + @Override + public float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return 0; + } + } + + /** + * Interface implementation for devices with at least v5 APIs. + */ + static class EclairMotionEventVersionImpl extends BaseMotionEventVersionImpl { + @Override + public int findPointerIndex(MotionEvent event, int pointerId) { + return MotionEventCompatEclair.findPointerIndex(event, pointerId); + } + @Override + public int getPointerId(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getPointerId(event, pointerIndex); + } + @Override + public float getX(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getX(event, pointerIndex); + } + @Override + public float getY(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getY(event, pointerIndex); + } + @Override + public int getPointerCount(MotionEvent event) { + return MotionEventCompatEclair.getPointerCount(event); + } + } + + /** + * Interface implementation for devices with at least v8 APIs. + */ + static class GingerbreadMotionEventVersionImpl extends EclairMotionEventVersionImpl { + @Override + public int getSource(MotionEvent event) { + return MotionEventCompatGingerbread.getSource(event); + } + } + + /** + * Interface implementation for devices with at least v12 APIs. + */ + static class HoneycombMr1MotionEventVersionImpl extends GingerbreadMotionEventVersionImpl { + + @Override + public float getAxisValue(MotionEvent event, int axis) { + return MotionEventCompatHoneycombMr1.getAxisValue(event, axis); + } + + @Override + public float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return MotionEventCompatHoneycombMr1.getAxisValue(event, axis, pointerIndex); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final MotionEventVersionImpl IMPL; + static { + if (Build.VERSION.SDK_INT >= 12) { + IMPL = new HoneycombMr1MotionEventVersionImpl(); + } else if (Build.VERSION.SDK_INT >= 9) { + IMPL = new GingerbreadMotionEventVersionImpl(); + } else if (Build.VERSION.SDK_INT >= 5) { + IMPL = new EclairMotionEventVersionImpl(); + } else { + IMPL = new BaseMotionEventVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Synonym for {@link MotionEvent#ACTION_MASK}. + */ + public static final int ACTION_MASK = 0xff; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_DOWN}. + */ + public static final int ACTION_POINTER_DOWN = 5; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_UP}. + */ + public static final int ACTION_POINTER_UP = 6; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_MOVE}. + */ + public static final int ACTION_HOVER_MOVE = 7; + + /** + * Synonym for {@link MotionEvent#ACTION_SCROLL}. + */ + public static final int ACTION_SCROLL = 8; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_INDEX_MASK}. + */ + public static final int ACTION_POINTER_INDEX_MASK = 0xff00; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_INDEX_SHIFT}. + */ + public static final int ACTION_POINTER_INDEX_SHIFT = 8; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_ENTER}. + */ + public static final int ACTION_HOVER_ENTER = 9; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_EXIT}. + */ + public static final int ACTION_HOVER_EXIT = 10; + + /** + * Synonym for {@link MotionEvent#AXIS_X}. + */ + public static final int AXIS_X = 0; + + /** + * Synonym for {@link MotionEvent#AXIS_Y}. + */ + public static final int AXIS_Y = 1; + + /** + * Synonym for {@link MotionEvent#AXIS_PRESSURE}. + */ + public static final int AXIS_PRESSURE = 2; + + /** + * Synonym for {@link MotionEvent#AXIS_SIZE}. + */ + public static final int AXIS_SIZE = 3; + + /** + * Synonym for {@link MotionEvent#AXIS_TOUCH_MAJOR}. + */ + public static final int AXIS_TOUCH_MAJOR = 4; + + /** + * Synonym for {@link MotionEvent#AXIS_TOUCH_MINOR}. + */ + public static final int AXIS_TOUCH_MINOR = 5; + + /** + * Synonym for {@link MotionEvent#AXIS_TOOL_MAJOR}. + */ + public static final int AXIS_TOOL_MAJOR = 6; + + /** + * Synonym for {@link MotionEvent#AXIS_TOOL_MINOR}. + */ + public static final int AXIS_TOOL_MINOR = 7; + + /** + * Synonym for {@link MotionEvent#AXIS_ORIENTATION}. + */ + public static final int AXIS_ORIENTATION = 8; + + /** + * Synonym for {@link MotionEvent#AXIS_VSCROLL}. + */ + public static final int AXIS_VSCROLL = 9; + + /** + * Synonym for {@link MotionEvent#AXIS_HSCROLL}. + */ + public static final int AXIS_HSCROLL = 10; + + /** + * Synonym for {@link MotionEvent#AXIS_Z}. + */ + public static final int AXIS_Z = 11; + + /** + * Synonym for {@link MotionEvent#AXIS_RX}. + */ + public static final int AXIS_RX = 12; + + /** + * Synonym for {@link MotionEvent#AXIS_RY}. + */ + public static final int AXIS_RY = 13; + + /** + * Synonym for {@link MotionEvent#AXIS_RZ}. + */ + public static final int AXIS_RZ = 14; + + /** + * Synonym for {@link MotionEvent#AXIS_HAT_X}. + */ + public static final int AXIS_HAT_X = 15; + + /** + * Synonym for {@link MotionEvent#AXIS_HAT_Y}. + */ + public static final int AXIS_HAT_Y = 16; + + /** + * Synonym for {@link MotionEvent#AXIS_LTRIGGER}. + */ + public static final int AXIS_LTRIGGER = 17; + + /** + * Synonym for {@link MotionEvent#AXIS_RTRIGGER}. + */ + public static final int AXIS_RTRIGGER = 18; + + /** + * Synonym for {@link MotionEvent#AXIS_THROTTLE}. + */ + public static final int AXIS_THROTTLE = 19; + + /** + * Synonym for {@link MotionEvent#AXIS_RUDDER}. + */ + public static final int AXIS_RUDDER = 20; + + /** + * Synonym for {@link MotionEvent#AXIS_WHEEL}. + */ + public static final int AXIS_WHEEL = 21; + + /** + * Synonym for {@link MotionEvent#AXIS_GAS}. + */ + public static final int AXIS_GAS = 22; + + /** + * Synonym for {@link MotionEvent#AXIS_BRAKE}. + */ + public static final int AXIS_BRAKE = 23; + + /** + * Synonym for {@link MotionEvent#AXIS_DISTANCE}. + */ + public static final int AXIS_DISTANCE = 24; + + /** + * Synonym for {@link MotionEvent#AXIS_TILT}. + */ + public static final int AXIS_TILT = 25; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_1}. + */ + public static final int AXIS_GENERIC_1 = 32; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_2}. + */ + public static final int AXIS_GENERIC_2 = 33; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_3}. + */ + public static final int AXIS_GENERIC_3 = 34; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_4}. + */ + public static final int AXIS_GENERIC_4 = 35; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_5}. + */ + public static final int AXIS_GENERIC_5 = 36; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_6}. + */ + public static final int AXIS_GENERIC_6 = 37; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_7}. + */ + public static final int AXIS_GENERIC_7 = 38; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_8}. + */ + public static final int AXIS_GENERIC_8 = 39; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_9}. + */ + public static final int AXIS_GENERIC_9 = 40; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_10}. + */ + public static final int AXIS_GENERIC_10 = 41; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_11}. + */ + public static final int AXIS_GENERIC_11 = 42; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_12}. + */ + public static final int AXIS_GENERIC_12 = 43; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_13}. + */ + public static final int AXIS_GENERIC_13 = 44; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_14}. + */ + public static final int AXIS_GENERIC_14 = 45; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_15}. + */ + public static final int AXIS_GENERIC_15 = 46; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_16}. + */ + public static final int AXIS_GENERIC_16 = 47; + + /** + * Call {@link MotionEvent#getAction}, returning only the {@link #ACTION_MASK} + * portion. + */ + public static int getActionMasked(MotionEvent event) { + return event.getAction() & ACTION_MASK; + } + + /** + * Call {@link MotionEvent#getAction}, returning only the pointer index + * portion + */ + public static int getActionIndex(MotionEvent event) { + return (event.getAction() & ACTION_POINTER_INDEX_MASK) + >> ACTION_POINTER_INDEX_SHIFT; + } + + /** + * Call {@link MotionEvent#findPointerIndex(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * does nothing and returns -1. + */ + public static int findPointerIndex(MotionEvent event, int pointerId) { + return IMPL.findPointerIndex(event, pointerId); + } + + /** + * Call {@link MotionEvent#getPointerId(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static int getPointerId(MotionEvent event, int pointerIndex) { + return IMPL.getPointerId(event, pointerIndex); + } + + /** + * Call {@link MotionEvent#getX(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static float getX(MotionEvent event, int pointerIndex) { + return IMPL.getX(event, pointerIndex); + } + + /** + * Call {@link MotionEvent#getY(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static float getY(MotionEvent event, int pointerIndex) { + return IMPL.getY(event, pointerIndex); + } + + /** + * The number of pointers of data contained in this event. Always + * >= 1. + */ + public static int getPointerCount(MotionEvent event) { + return IMPL.getPointerCount(event); + } + + /** + * Gets the source of the event. + * + * @return The event source or {@link InputDeviceCompat#SOURCE_UNKNOWN} if unknown. + */ + public static int getSource(MotionEvent event) { + return IMPL.getSource(event); + } + + /** + * Get axis value for the first pointer index (may be an + * arbitrary pointer identifier). + * + * @param axis The axis identifier for the axis value to retrieve. + * + * @see #AXIS_X + * @see #AXIS_Y + */ + public static float getAxisValue(MotionEvent event, int axis) { + return IMPL.getAxisValue(event, axis); + } + + /** + * Returns the value of the requested axis for the given pointer index + * (use {@link #getPointerId(MotionEvent, int)} to find the pointer identifier for this index). + * + * @param axis The axis identifier for the axis value to retrieve. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount(MotionEvent)}-1. + * @return The value of the axis, or 0 if the axis is not available. + * + * @see #AXIS_X + * @see #AXIS_Y + */ + public static float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return IMPL.getAxisValue(event, axis, pointerIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java new file mode 100644 index 00000000000..7ecfb5cf84f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.MotionEvent; + +/** + * Implementation of motion event compatibility that can call Eclair APIs. + */ +class MotionEventCompatEclair { + public static int findPointerIndex(MotionEvent event, int pointerId) { + return event.findPointerIndex(pointerId); + } + public static int getPointerId(MotionEvent event, int pointerIndex) { + return event.getPointerId(pointerIndex); + } + public static float getX(MotionEvent event, int pointerIndex) { + return event.getX(pointerIndex); + } + public static float getY(MotionEvent event, int pointerIndex) { + return event.getY(pointerIndex); + } + public static int getPointerCount(MotionEvent event) { + return event.getPointerCount(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java new file mode 100644 index 00000000000..f43dc0a0747 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java @@ -0,0 +1,28 @@ +/* + * 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.v4.view; + +import android.view.MotionEvent; + +/** + * Motion event compatibility class for API 8+. + */ +class MotionEventCompatGingerbread { + public static int getSource(MotionEvent event) { + return event.getSource(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java new file mode 100644 index 00000000000..406fcf32ed9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java @@ -0,0 +1,32 @@ +/* + * 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.v4.view; + +import android.view.MotionEvent; + +/** + * Motion event compatibility class for API 12+. + */ +class MotionEventCompatHoneycombMr1 { + static float getAxisValue(MotionEvent event, int axis) { + return event.getAxisValue(axis); + } + + static float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return event.getAxisValue(axis, pointerIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java new file mode 100644 index 00000000000..40e0a09fdf3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java @@ -0,0 +1,227 @@ +/* + * 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.v4.view; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +/** + * This interface should be implemented by {@link android.view.View View} subclasses that wish + * to support dispatching nested scrolling operations to a cooperating parent + * {@link android.view.ViewGroup ViewGroup}. + * + *

    Classes implementing this interface should create a final instance of a + * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the + * NestedScrollingChildHelper methods of the same signature.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public interface NestedScrollingChild { + /** + * Enable or disable nested scrolling for this view. + * + *

    If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.

    + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled() + */ + public void setNestedScrollingEnabled(boolean enabled); + + /** + * Returns true if nested scrolling is enabled for this view. + * + *

    If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.

    + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(boolean) + */ + public boolean isNestedScrollingEnabled(); + + /** + * Begin a nestable scroll operation along the given axes. + * + *

    A view starting a nested scroll promises to abide by the following contract:

    + * + *

    The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll()} to indicate the end of the nested scroll.

    + * + *

    If startNestedScroll returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.

    + * + *

    At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.

    + * + *

    After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + *

    + * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll() + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean startNestedScroll(int axes); + + /** + * Stop a nested scroll in progress. + * + *

    Calling this method when a nested scroll is not currently in progress is harmless.

    + * + * @see #startNestedScroll(int) + */ + public void stopNestedScroll(); + + /** + * Returns true if this view has a nested scrolling parent. + * + *

    The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.

    + * + * @return whether this view has a nested scrolling parent + */ + public boolean hasNestedScrollingParent(); + + /** + * Dispatch one step of a nested scroll in progress. + * + *

    Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.

    + * + *

    Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.

    + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + */ + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + *

    Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * dispatchNestedPreScroll offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.

    + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); + + /** + * Dispatch a fling to a nested scrolling parent. + * + *

    This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + *

    Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. dispatchNestedPreFling + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns true, a nested + * parent view consumed the fling and this view should not scroll as a result.

    + * + *

    For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:

    + * + *
      + *
    • If a custom view is paged and needs to settle to a fixed page-point, do not + * call dispatchNestedPreFling; consume the fling and settle to a valid + * position regardless.
    • + *
    • If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.
    • + *
    + * + *

    Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + public boolean dispatchNestedPreFling(float velocityX, float velocityY); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java new file mode 100644 index 00000000000..9e25667756a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java @@ -0,0 +1,282 @@ +/* + * 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.v4.view; + +import android.view.View; +import android.view.ViewParent; + +/** + * Helper class for implementing nested scrolling child views compatible with Android platform + * versions earlier than Android 5.0 Lollipop (API 21). + * + *

    {@link android.view.View View} subclasses should instantiate a final instance of this + * class as a field at construction. For each View method that has a matching + * method signature in this class, delegate the operation to the helper instance in an overriden + * method implementation. This implements the standard framework policy for nested scrolling.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public class NestedScrollingChildHelper { + private final View mView; + private ViewParent mNestedScrollingParent; + private boolean mIsNestedScrollingEnabled; + private int[] mTempNestedScrollConsumed; + + /** + * Construct a new helper for a given view. + */ + public NestedScrollingChildHelper(View view) { + mView = view; + } + + /** + * Enable nested scrolling. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param enabled true to enable nested scrolling dispatch from this view, false otherwise + */ + public void setNestedScrollingEnabled(boolean enabled) { + if (mIsNestedScrollingEnabled) { + ViewCompat.stopNestedScroll(mView); + } + mIsNestedScrollingEnabled = enabled; + } + + /** + * Check if nested scrolling is enabled for this view. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if nested scrolling is enabled for this view + */ + public boolean isNestedScrollingEnabled() { + return mIsNestedScrollingEnabled; + } + + /** + * Check if this view has a nested scrolling parent view currently receiving events for + * a nested scroll in progress. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if this view has a nested scrolling parent, false otherwise + */ + public boolean hasNestedScrollingParent() { + return mNestedScrollingParent != null; + } + + /** + * Start a new nested scroll for this view. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param axes Supported nested scroll axes. + * See {@link NestedScrollingChild#startNestedScroll(int)}. + * @return true if a cooperating parent view was found and nested scrolling started successfully + */ + public boolean startNestedScroll(int axes) { + if (hasNestedScrollingParent()) { + // Already in progress + return true; + } + if (isNestedScrollingEnabled()) { + ViewParent p = mView.getParent(); + View child = mView; + while (p != null) { + if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { + mNestedScrollingParent = p; + ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); + return true; + } + if (p instanceof View) { + child = (View) p; + } + p = p.getParent(); + } + } + return false; + } + + /** + * Stop a nested scroll in progress. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + */ + public void stopNestedScroll() { + if (mNestedScrollingParent != null) { + ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView); + mNestedScrollingParent = null; + } + } + + /** + * Dispatch one step of a nested scrolling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed any of the nested scroll + */ + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, + dyConsumed, dxUnconsumed, dyUnconsumed); + + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return true; + } else if (offsetInWindow != null) { + // No motion, no dispatch. Keep offsetInWindow up to date. + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed any of the nested scroll + */ + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dx != 0 || dy != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + if (consumed == null) { + if (mTempNestedScrollConsumed == null) { + mTempNestedScrollConsumed = new int[2]; + } + consumed = mTempNestedScrollConsumed; + } + consumed[0] = 0; + consumed[1] = 0; + ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); + + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return consumed[0] != 0 || consumed[1] != 0; + } else if (offsetInWindow != null) { + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch a nested fling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed the nested fling + */ + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, + velocityY, consumed); + } + return false; + } + + /** + * Dispatch a nested pre-fling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed the nested fling + */ + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, + velocityY); + } + return false; + } + + /** + * View subclasses should always call this method on their + * NestedScrollingChildHelper when detached from a window. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + */ + public void onDetachedFromWindow() { + ViewCompat.stopNestedScroll(mView); + } + + /** + * Called when a nested scrolling child stops its current nested scroll operation. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param child Child view stopping its nested scroll. This may not be a direct child view. + */ + public void onStopNestedScroll(View child) { + ViewCompat.stopNestedScroll(mView); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java new file mode 100644 index 00000000000..bc056b6d1fb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java @@ -0,0 +1,196 @@ +/* + * 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.v4.view; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses + * that wish to support scrolling operations delegated by a nested child view. + * + *

    Classes implementing this interface should create a final instance of a + * {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods + * to the NestedScrollingParentHelper methods of the same signature.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public interface NestedScrollingParent { + /** + * React to a descendant view initiating a nestable scroll operation, claiming the + * nested scroll operation if appropriate. + * + *

    This method will be called in response to a descendant view invoking + * {@link ViewCompat#startNestedScroll(View, int)}. Each parent up the view hierarchy will be + * given an opportunity to respond and claim the nested scrolling operation by returning + * true.

    + * + *

    This method may be overridden by ViewParent implementations to indicate when the view + * is willing to support a nested scrolling operation that is about to begin. If it returns + * true, this ViewParent will become the target view's nested scrolling parent for the duration + * of the scroll operation in progress. When the nested scroll is finished this ViewParent + * will receive a call to {@link #onStopNestedScroll(View)}. + *

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @return true if this ViewParent accepts the nested scroll operation + */ + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); + + /** + * React to the successful claiming of a nested scroll operation. + * + *

    This method will be called after + * {@link #onStartNestedScroll(View, View, int) onStartNestedScroll} returns true. It offers + * an opportunity for the view and its superclasses to perform initial configuration + * for the nested scroll. Implementations of this method should always call their superclass's + * implementation of this method if one is present.

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @see #onStartNestedScroll(View, View, int) + * @see #onStopNestedScroll(View) + */ + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); + + /** + * React to a nested scroll operation ending. + * + *

    Perform cleanup after a nested scrolling operation. + * This method will be called when a nested scroll stops, for example when a nested touch + * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event. + * Implementations of this method should always call their superclass's implementation of this + * method if one is present.

    + * + * @param target View that initiated the nested scroll + */ + public void onStopNestedScroll(View target); + + /** + * React to a nested scroll in progress. + * + *

    This method will be called when the ViewParent's current nested scrolling child view + * dispatches a nested scroll event. To receive calls to this method the ViewParent must have + * previously returned true for a call to + * {@link #onStartNestedScroll(View, View, int)}.

    + * + *

    Both the consumed and unconsumed portions of the scroll distance are reported to the + * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll + * position of multiple child elements, for example. The unconsumed portion may be used to + * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling + * a list within a vertical drawer where the drawer begins dragging once the edge of inner + * scrolling content is reached.

    + * + * @param target The descendent view controlling the nested scroll + * @param dxConsumed Horizontal scroll distance in pixels already consumed by target + * @param dyConsumed Vertical scroll distance in pixels already consumed by target + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target + * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target + */ + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + + /** + * React to a nested scroll in progress before the target view consumes a portion of the scroll. + * + *

    When working with nested scrolling often the parent view may want an opportunity + * to consume the scroll before the nested scrolling child does. An example of this is a + * drawer that contains a scrollable list. The user will want to be able to scroll the list + * fully into view before the list itself begins scrolling.

    + * + *

    onNestedPreScroll is called when a nested scrolling child invokes + * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should + * report how any pixels of the scroll reported by dx, dy were consumed in the + * consumed array. Index 0 corresponds to dx and index 1 corresponds to dy. + * This parameter will never be null. Initial values for consumed[0] and consumed[1] + * will always be 0.

    + * + * @param target View that initiated the nested scroll + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent + */ + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); + + /** + * Request a fling from a nested scroll. + * + *

    This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling + */ + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); + + /** + * React to a nested fling before the target view consumes it. + * + *

    This method siginfies that a nested scrolling child has detected a fling with the given + * velocity along each axis. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling parent is consuming motion as part of a + * {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for + * it to also consume the pre-fling to complete that same motion. By returning + * true from this method, the parent indicates that the child should not + * fling its own internal content as well.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @return true if this parent consumed the fling ahead of the target view + */ + public boolean onNestedPreFling(View target, float velocityX, float velocityY); + + /** + * Return the current axes of nested scrolling for this NestedScrollingParent. + * + *

    A NestedScrollingParent returning something other than {@link ViewCompat#SCROLL_AXIS_NONE} + * is currently acting as a nested scrolling parent for one or more descendant views in + * the hierarchy.

    + * + * @return Flags indicating the current axes of nested scrolling + * @see ViewCompat#SCROLL_AXIS_HORIZONTAL + * @see ViewCompat#SCROLL_AXIS_VERTICAL + * @see ViewCompat#SCROLL_AXIS_NONE + */ + public int getNestedScrollAxes(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java new file mode 100644 index 00000000000..91522f1ca85 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java @@ -0,0 +1,84 @@ +/* + * 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.v4.view; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Helper class for implementing nested scrolling parent views compatible with Android platform + * versions earlier than Android 5.0 Lollipop (API 21). + * + *

    {@link android.view.ViewGroup ViewGroup} subclasses should instantiate a final instance + * of this class as a field at construction. For each ViewGroup method that has + * a matching method signature in this class, delegate the operation to the helper instance + * in an overriden method implementation. This implements the standard framework policy + * for nested scrolling.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public class NestedScrollingParentHelper { + private final ViewGroup mViewGroup; + private int mNestedScrollAxes; + + /** + * Construct a new helper for a given ViewGroup + */ + public NestedScrollingParentHelper(ViewGroup viewGroup) { + mViewGroup = viewGroup; + } + + /** + * Called when a nested scrolling operation initiated by a descendant view is accepted + * by this ViewGroup. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + */ + public void onNestedScrollAccepted(View child, View target, int axes) { + mNestedScrollAxes = axes; + } + + /** + * Return the current axes of nested scrolling for this ViewGroup. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + */ + public int getNestedScrollAxes() { + return mNestedScrollAxes; + } + + /** + * React to a nested scroll operation ending. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + * + * @param target View that initiated the nested scroll + */ + public void onStopNestedScroll(View target) { + mNestedScrollAxes = 0; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java new file mode 100644 index 00000000000..2c2b95f890e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * Listener for applying window insets on a view in a custom way. + * + *

    Apps may choose to implement this interface if they want to apply custom policy + * to the way that window insets are treated for a view. If an OnApplyWindowInsetsListener + * is set, it's + * {@link #onApplyWindowInsets(android.view.View, WindowInsetsCompat) onApplyWindowInsets} + * method will be called instead of the View's own {@code onApplyWindowInsets} method. + * The listener may optionally call the parameter View's onApplyWindowInsets + * method to apply the View's normal behavior as part of its own.

    + */ +public interface OnApplyWindowInsetsListener { + /** + * When {@link ViewCompat#setOnApplyWindowInsetsListener(View, OnApplyWindowInsetsListener) set} + * on a View, this listener method will be called instead of the view's own + * {@code onApplyWindowInsets} method. + * + * @param v The view applying window insets + * @param insets The insets to apply + * @return The insets supplied, minus any insets that were consumed + */ + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets); +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java new file mode 100644 index 00000000000..ef524047d06 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base class providing the adapter to populate pages inside of + * a {@link ViewPager}. You will most likely want to use a more + * specific implementation of this, such as + * {@link android.support.v4.app.FragmentPagerAdapter} or + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + * + *

    When you implement a PagerAdapter, you must override the following methods + * at minimum:

    + *
      + *
    • {@link #instantiateItem(ViewGroup, int)}
    • + *
    • {@link #destroyItem(ViewGroup, int, Object)}
    • + *
    • {@link #getCount()}
    • + *
    • {@link #isViewFromObject(View, Object)}
    • + *
    + * + *

    PagerAdapter is more general than the adapters used for + * {@link android.widget.AdapterView AdapterViews}. Instead of providing a + * View recycling mechanism directly ViewPager uses callbacks to indicate the + * steps taken during an update. A PagerAdapter may implement a form of View + * recycling if desired or use a more sophisticated method of managing page + * Views such as Fragment transactions where each page is represented by its + * own Fragment.

    + * + *

    ViewPager associates each page with a key Object instead of working with + * Views directly. This key is used to track and uniquely identify a given page + * independent of its position in the adapter. A call to the PagerAdapter method + * {@link #startUpdate(ViewGroup)} indicates that the contents of the ViewPager + * are about to change. One or more calls to {@link #instantiateItem(ViewGroup, int)} + * and/or {@link #destroyItem(ViewGroup, int, Object)} will follow, and the end + * of an update will be signaled by a call to {@link #finishUpdate(ViewGroup)}. + * By the time {@link #finishUpdate(ViewGroup) finishUpdate} returns the views + * associated with the key objects returned by + * {@link #instantiateItem(ViewGroup, int) instantiateItem} should be added to + * the parent ViewGroup passed to these methods and the views associated with + * the keys passed to {@link #destroyItem(ViewGroup, int, Object) destroyItem} + * should be removed. The method {@link #isViewFromObject(View, Object)} identifies + * whether a page View is associated with a given key object.

    + * + *

    A very simple PagerAdapter may choose to use the page Views themselves + * as key objects, returning them from {@link #instantiateItem(ViewGroup, int)} + * after creation and adding them to the parent ViewGroup. A matching + * {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the + * View from the parent ViewGroup and {@link #isViewFromObject(View, Object)} + * could be implemented as return view == object;.

    + * + *

    PagerAdapter supports data set changes. Data set changes must occur on the + * main thread and must end with a call to {@link #notifyDataSetChanged()} similar + * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data + * set change may involve pages being added, removed, or changing position. The + * ViewPager will keep the current page active provided the adapter implements + * the method {@link #getItemPosition(Object)}.

    + */ +public abstract class PagerAdapter { + private DataSetObservable mObservable = new DataSetObservable(); + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(ViewGroup)} + */ + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(ViewGroup, int)} + */ + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)} + */ + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)} + */ + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(ViewGroup)} + */ + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with object + * @param object Object to check for association with view + * @return true if view is associated with the key object object + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + *

    The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java new file mode 100644 index 00000000000..834035c5d96 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java @@ -0,0 +1,296 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * PagerTabStrip is an interactive indicator of the current, next, + * and previous pages of a {@link ViewPager}. It is intended to be used as a + * child view of a ViewPager widget in your XML layout. + * Add it as a child of a ViewPager in your layout file and set its + * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom + * of the ViewPager. The title from each page is supplied by the method + * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to + * the ViewPager. + * + *

    For a non-interactive indicator, see {@link PagerTitleStrip}.

    + */ +public class PagerTabStrip extends PagerTitleStrip { + private static final String TAG = "PagerTabStrip"; + + private static final int INDICATOR_HEIGHT = 3; // dp + private static final int MIN_PADDING_BOTTOM = INDICATOR_HEIGHT + 3; // dp + private static final int TAB_PADDING = 16; // dp + private static final int TAB_SPACING = 32; // dp + private static final int MIN_TEXT_SPACING = TAB_SPACING + TAB_PADDING * 2; // dp + private static final int FULL_UNDERLINE_HEIGHT = 1; // dp + private static final int MIN_STRIP_HEIGHT = 32; // dp + + private int mIndicatorColor; + private int mIndicatorHeight; + + private int mMinPaddingBottom; + private int mMinTextSpacing; + private int mMinStripHeight; + + private int mTabPadding; + + private final Paint mTabPaint = new Paint(); + private final Rect mTempRect = new Rect(); + + private int mTabAlpha = 0xFF; + + private boolean mDrawFullUnderline = false; + private boolean mDrawFullUnderlineSet = false; + private int mFullUnderlineHeight; + + private boolean mIgnoreTap; + private float mInitialMotionX; + private float mInitialMotionY; + private int mTouchSlop; + + public PagerTabStrip(Context context) { + this(context, null); + } + + public PagerTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + mIndicatorColor = mTextColor; + mTabPaint.setColor(mIndicatorColor); + + // Note: this follows the rules for Resources#getDimensionPixelOffset/Size: + // sizes round up, offsets round down. + final float density = context.getResources().getDisplayMetrics().density; + mIndicatorHeight = (int) (INDICATOR_HEIGHT * density + 0.5f); + mMinPaddingBottom = (int) (MIN_PADDING_BOTTOM * density + 0.5f); + mMinTextSpacing = (int) (MIN_TEXT_SPACING * density); + mTabPadding = (int) (TAB_PADDING * density + 0.5f); + mFullUnderlineHeight = (int) (FULL_UNDERLINE_HEIGHT * density + 0.5f); + mMinStripHeight = (int) (MIN_STRIP_HEIGHT * density + 0.5f); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + // Enforce restrictions + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + setTextSpacing(getTextSpacing()); + + setWillNotDraw(false); + + mPrevText.setFocusable(true); + mPrevText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } + }); + + mNextText.setFocusable(true); + mNextText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + }); + + if (getBackground() == null) { + mDrawFullUnderline = true; + } + } + + /** + * Set the color of the tab indicator bar. + * + * @param color Color to set as an 0xRRGGBB value. The high byte (alpha) is ignored. + */ + public void setTabIndicatorColor(int color) { + mIndicatorColor = color; + mTabPaint.setColor(mIndicatorColor); + invalidate(); + } + + /** + * Set the color of the tab indicator bar from a color resource. + * + * @param resId Resource ID of a color resource to load + */ + public void setTabIndicatorColorResource(@ColorRes int resId) { + setTabIndicatorColor(getContext().getResources().getColor(resId)); + } + + /** + * @return The current tab indicator color as an 0xRRGGBB value. + */ + public int getTabIndicatorColor() { + return mIndicatorColor; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + if (bottom < mMinPaddingBottom) { + bottom = mMinPaddingBottom; + } + super.setPadding(left, top, right, bottom); + } + + @Override + public void setTextSpacing(int textSpacing) { + if (textSpacing < mMinTextSpacing) { + textSpacing = mMinTextSpacing; + } + super.setTextSpacing(textSpacing); + } + + @Override + public void setBackgroundDrawable(Drawable d) { + super.setBackgroundDrawable(d); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = d == null; + } + } + + @Override + public void setBackgroundColor(int color) { + super.setBackgroundColor(color); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = (color & 0xFF000000) == 0; + } + } + + @Override + public void setBackgroundResource(@DrawableRes int resId) { + super.setBackgroundResource(resId); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = resId == 0; + } + } + + /** + * Set whether this tab strip should draw a full-width underline in the + * current tab indicator color. + * + * @param drawFull true to draw a full-width underline, false otherwise + */ + public void setDrawFullUnderline(boolean drawFull) { + mDrawFullUnderline = drawFull; + mDrawFullUnderlineSet = true; + invalidate(); + } + + /** + * Return whether or not this tab strip will draw a full-width underline. + * This defaults to true if no background is set. + * + * @return true if this tab strip will draw a full-width underline in the + * current tab indicator color. + */ + public boolean getDrawFullUnderline() { + return mDrawFullUnderline; + } + + @Override + int getMinHeight() { + return Math.max(super.getMinHeight(), mMinStripHeight); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + if (action != MotionEvent.ACTION_DOWN && mIgnoreTap) { + return false; + } + + // Any tap within touch slop to either side of the current item + // will scroll to prev/next. + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mInitialMotionX = x; + mInitialMotionY = y; + mIgnoreTap = false; + break; + + case MotionEvent.ACTION_MOVE: + if (Math.abs(x - mInitialMotionX) > mTouchSlop || + Math.abs(y - mInitialMotionY) > mTouchSlop) { + mIgnoreTap = true; + } + break; + + case MotionEvent.ACTION_UP: + if (x < mCurrText.getLeft() - mTabPadding) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } else if (x > mCurrText.getRight() + mTabPadding) { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + break; + } + + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int height = getHeight(); + final int bottom = height; + final int left = mCurrText.getLeft() - mTabPadding; + final int right = mCurrText.getRight() + mTabPadding; + final int top = bottom - mIndicatorHeight; + + mTabPaint.setColor(mTabAlpha << 24 | (mIndicatorColor & 0xFFFFFF)); + canvas.drawRect(left, top, right, bottom, mTabPaint); + + if (mDrawFullUnderline) { + mTabPaint.setColor(0xFF << 24 | (mIndicatorColor & 0xFFFFFF)); + canvas.drawRect(getPaddingLeft(), height - mFullUnderlineHeight, + getWidth() - getPaddingRight(), height, mTabPaint); + } + } + + @Override + void updateTextPositions(int position, float positionOffset, boolean force) { + final Rect r = mTempRect; + int bottom = getHeight(); + int left = mCurrText.getLeft() - mTabPadding; + int right = mCurrText.getRight() + mTabPadding; + int top = bottom - mIndicatorHeight; + + r.set(left, top, right, bottom); + + super.updateTextPositions(position, positionOffset, force); + mTabAlpha = (int) (Math.abs(positionOffset - 0.5f) * 2 * 0xFF); + + left = mCurrText.getLeft() - mTabPadding; + right = mCurrText.getRight() + mTabPadding; + r.union(left, top, right, bottom); + + invalidate(r); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java new file mode 100644 index 00000000000..79c771f43f5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/** + * PagerTitleStrip is a non-interactive indicator of the current, next, + * and previous pages of a {@link ViewPager}. It is intended to be used as a + * child view of a ViewPager widget in your XML layout. + * Add it as a child of a ViewPager in your layout file and set its + * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom + * of the ViewPager. The title from each page is supplied by the method + * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to + * the ViewPager. + * + *

    For an interactive indicator, see {@link PagerTabStrip}.

    + */ +public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor { + private static final String TAG = "PagerTitleStrip"; + + ViewPager mPager; + TextView mPrevText; + TextView mCurrText; + TextView mNextText; + + private int mLastKnownCurrentPage = -1; + private float mLastKnownPositionOffset = -1; + private int mScaledTextSpacing; + private int mGravity; + + private boolean mUpdatingText; + private boolean mUpdatingPositions; + + private final PageListener mPageListener = new PageListener(); + + private WeakReference mWatchingAdapter; + + private static final int[] ATTRS = new int[] { + android.R.attr.textAppearance, + android.R.attr.textSize, + android.R.attr.textColor, + android.R.attr.gravity + }; + + private static final int[] TEXT_ATTRS = new int[] { + 0x0101038c // android.R.attr.textAllCaps + }; + + private static final float SIDE_ALPHA = 0.6f; + private static final int TEXT_SPACING = 16; // dip + + private int mNonPrimaryAlpha; + int mTextColor; + + interface PagerTitleStripImpl { + void setSingleLineAllCaps(TextView text); + } + + static class PagerTitleStripImplBase implements PagerTitleStripImpl { + public void setSingleLineAllCaps(TextView text) { + text.setSingleLine(); + } + } + + static class PagerTitleStripImplIcs implements PagerTitleStripImpl { + public void setSingleLineAllCaps(TextView text) { + PagerTitleStripIcs.setSingleLineAllCaps(text); + } + } + + private static final PagerTitleStripImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new PagerTitleStripImplIcs(); + } else { + IMPL = new PagerTitleStripImplBase(); + } + } + + private static void setSingleLineAllCaps(TextView text) { + IMPL.setSingleLineAllCaps(text); + } + + public PagerTitleStrip(Context context) { + this(context, null); + } + + public PagerTitleStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + addView(mPrevText = new TextView(context)); + addView(mCurrText = new TextView(context)); + addView(mNextText = new TextView(context)); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + final int textAppearance = a.getResourceId(0, 0); + if (textAppearance != 0) { + mPrevText.setTextAppearance(context, textAppearance); + mCurrText.setTextAppearance(context, textAppearance); + mNextText.setTextAppearance(context, textAppearance); + } + final int textSize = a.getDimensionPixelSize(1, 0); + if (textSize != 0) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + if (a.hasValue(2)) { + final int textColor = a.getColor(2, 0); + mPrevText.setTextColor(textColor); + mCurrText.setTextColor(textColor); + mNextText.setTextColor(textColor); + } + mGravity = a.getInteger(3, Gravity.BOTTOM); + a.recycle(); + + mTextColor = mCurrText.getTextColors().getDefaultColor(); + setNonPrimaryAlpha(SIDE_ALPHA); + + mPrevText.setEllipsize(TruncateAt.END); + mCurrText.setEllipsize(TruncateAt.END); + mNextText.setEllipsize(TruncateAt.END); + + boolean allCaps = false; + if (textAppearance != 0) { + final TypedArray ta = context.obtainStyledAttributes(textAppearance, TEXT_ATTRS); + allCaps = ta.getBoolean(0, false); + ta.recycle(); + } + + if (allCaps) { + setSingleLineAllCaps(mPrevText); + setSingleLineAllCaps(mCurrText); + setSingleLineAllCaps(mNextText); + } else { + mPrevText.setSingleLine(); + mCurrText.setSingleLine(); + mNextText.setSingleLine(); + } + + final float density = context.getResources().getDisplayMetrics().density; + mScaledTextSpacing = (int) (TEXT_SPACING * density); + } + + /** + * Set the required spacing between title segments. + * + * @param spacingPixels Spacing between each title displayed in pixels + */ + public void setTextSpacing(int spacingPixels) { + mScaledTextSpacing = spacingPixels; + requestLayout(); + } + + /** + * @return The required spacing between title segments in pixels + */ + public int getTextSpacing() { + return mScaledTextSpacing; + } + + /** + * Set the alpha value used for non-primary page titles. + * + * @param alpha Opacity value in the range 0-1f + */ + public void setNonPrimaryAlpha(float alpha) { + mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF; + final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); + mPrevText.setTextColor(transparentColor); + mNextText.setTextColor(transparentColor); + } + + /** + * Set the color value used as the base color for all displayed page titles. + * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}. + * + * @param color Color hex code in 0xAARRGGBB format + */ + public void setTextColor(int color) { + mTextColor = color; + mCurrText.setTextColor(color); + final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); + mPrevText.setTextColor(transparentColor); + mNextText.setTextColor(transparentColor); + } + + /** + * Set the default text size to a given unit and value. + * See {@link TypedValue} for the possible dimension units. + * + *

    Example: to set the text size to 14px, use + * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);

    + * + * @param unit The desired dimension unit + * @param size The desired size in the given units + */ + public void setTextSize(int unit, float size) { + mPrevText.setTextSize(unit, size); + mCurrText.setTextSize(unit, size); + mNextText.setTextSize(unit, size); + } + + /** + * Set the {@link Gravity} used to position text within the title strip. + * Only the vertical gravity component is used. + * + * @param gravity {@link Gravity} constant for positioning title text + */ + public void setGravity(int gravity) { + mGravity = gravity; + requestLayout(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final ViewParent parent = getParent(); + if (!(parent instanceof ViewPager)) { + throw new IllegalStateException( + "PagerTitleStrip must be a direct child of a ViewPager."); + } + + final ViewPager pager = (ViewPager) parent; + final PagerAdapter adapter = pager.getAdapter(); + + pager.setInternalPageChangeListener(mPageListener); + pager.setOnAdapterChangeListener(mPageListener); + mPager = pager; + updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mPager != null) { + updateAdapter(mPager.getAdapter(), null); + mPager.setInternalPageChangeListener(null); + mPager.setOnAdapterChangeListener(null); + mPager = null; + } + } + + void updateText(int currentItem, PagerAdapter adapter) { + final int itemCount = adapter != null ? adapter.getCount() : 0; + mUpdatingText = true; + + CharSequence text = null; + if (currentItem >= 1 && adapter != null) { + text = adapter.getPageTitle(currentItem - 1); + } + mPrevText.setText(text); + + mCurrText.setText(adapter != null && currentItem < itemCount ? + adapter.getPageTitle(currentItem) : null); + + text = null; + if (currentItem + 1 < itemCount && adapter != null) { + text = adapter.getPageTitle(currentItem + 1); + } + mNextText.setText(text); + + // Measure everything + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f), + MeasureSpec.AT_MOST); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); + mPrevText.measure(childWidthSpec, childHeightSpec); + mCurrText.measure(childWidthSpec, childHeightSpec); + mNextText.measure(childWidthSpec, childHeightSpec); + + mLastKnownCurrentPage = currentItem; + + if (!mUpdatingPositions) { + updateTextPositions(currentItem, mLastKnownPositionOffset, false); + } + + mUpdatingText = false; + } + + @Override + public void requestLayout() { + if (!mUpdatingText) { + super.requestLayout(); + } + } + + void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) { + if (oldAdapter != null) { + oldAdapter.unregisterDataSetObserver(mPageListener); + mWatchingAdapter = null; + } + if (newAdapter != null) { + newAdapter.registerDataSetObserver(mPageListener); + mWatchingAdapter = new WeakReference(newAdapter); + } + if (mPager != null) { + mLastKnownCurrentPage = -1; + mLastKnownPositionOffset = -1; + updateText(mPager.getCurrentItem(), newAdapter); + requestLayout(); + } + } + + void updateTextPositions(int position, float positionOffset, boolean force) { + if (position != mLastKnownCurrentPage) { + updateText(position, mPager.getAdapter()); + } else if (!force && positionOffset == mLastKnownPositionOffset) { + return; + } + + mUpdatingPositions = true; + + final int prevWidth = mPrevText.getMeasuredWidth(); + final int currWidth = mCurrText.getMeasuredWidth(); + final int nextWidth = mNextText.getMeasuredWidth(); + final int halfCurrWidth = currWidth / 2; + + final int stripWidth = getWidth(); + final int stripHeight = getHeight(); + final int paddingLeft = getPaddingLeft(); + final int paddingRight = getPaddingRight(); + final int paddingTop = getPaddingTop(); + final int paddingBottom = getPaddingBottom(); + final int textPaddedLeft = paddingLeft + halfCurrWidth; + final int textPaddedRight = paddingRight + halfCurrWidth; + final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight; + + float currOffset = positionOffset + 0.5f; + if (currOffset > 1.f) { + currOffset -= 1.f; + } + final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset); + final int currLeft = currCenter - currWidth / 2; + final int currRight = currLeft + currWidth; + + final int prevBaseline = mPrevText.getBaseline(); + final int currBaseline = mCurrText.getBaseline(); + final int nextBaseline = mNextText.getBaseline(); + final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline); + final int prevTopOffset = maxBaseline - prevBaseline; + final int currTopOffset = maxBaseline - currBaseline; + final int nextTopOffset = maxBaseline - nextBaseline; + final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight(); + final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight(); + final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight(); + final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight), + alignedNextHeight); + + final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + int prevTop; + int currTop; + int nextTop; + switch (vgrav) { + default: + case Gravity.TOP: + prevTop = paddingTop + prevTopOffset; + currTop = paddingTop + currTopOffset; + nextTop = paddingTop + nextTopOffset; + break; + case Gravity.CENTER_VERTICAL: + final int paddedHeight = stripHeight - paddingTop - paddingBottom; + final int centeredTop = (paddedHeight - maxTextHeight) / 2; + prevTop = centeredTop + prevTopOffset; + currTop = centeredTop + currTopOffset; + nextTop = centeredTop + nextTopOffset; + break; + case Gravity.BOTTOM: + final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; + prevTop = bottomGravTop + prevTopOffset; + currTop = bottomGravTop + currTopOffset; + nextTop = bottomGravTop + nextTopOffset; + break; + } + + mCurrText.layout(currLeft, currTop, currRight, + currTop + mCurrText.getMeasuredHeight()); + + final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth); + mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth, + prevTop + mPrevText.getMeasuredHeight()); + + final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth, + currRight + mScaledTextSpacing); + mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth, + nextTop + mNextText.getMeasuredHeight()); + + mLastKnownPositionOffset = positionOffset; + mUpdatingPositions = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Must measure with an exact width"); + } + + int childHeight = heightSize; + int minHeight = getMinHeight(); + int padding = 0; + padding = getPaddingTop() + getPaddingBottom(); + childHeight -= padding; + + final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f), + MeasureSpec.AT_MOST); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); + + mPrevText.measure(childWidthSpec, childHeightSpec); + mCurrText.measure(childWidthSpec, childHeightSpec); + mNextText.measure(childWidthSpec, childHeightSpec); + + if (heightMode == MeasureSpec.EXACTLY) { + setMeasuredDimension(widthSize, heightSize); + } else { + int textHeight = mCurrText.getMeasuredHeight(); + setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding)); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mPager != null) { + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mLastKnownCurrentPage, offset, true); + } + } + + int getMinHeight() { + int minHeight = 0; + final Drawable bg = getBackground(); + if (bg != null) { + minHeight = bg.getIntrinsicHeight(); + } + return minHeight; + } + + private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener, + ViewPager.OnAdapterChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (positionOffset > 0.5f) { + // Consider ourselves to be on the next page when we're 50% of the way there. + position++; + } + updateTextPositions(position, positionOffset, false); + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + // Only update the text here if we're not dragging or settling. + updateText(mPager.getCurrentItem(), mPager.getAdapter()); + + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mPager.getCurrentItem(), offset, true); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + } + + @Override + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) { + updateAdapter(oldAdapter, newAdapter); + } + + @Override + public void onChanged() { + updateText(mPager.getCurrentItem(), mPager.getAdapter()); + + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mPager.getCurrentItem(), offset, true); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java new file mode 100644 index 00000000000..99cc8677265 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.text.method.SingleLineTransformationMethod; +import android.view.View; +import android.widget.TextView; + +import java.util.Locale; + +class PagerTitleStripIcs { + public static void setSingleLineAllCaps(TextView text) { + text.setTransformationMethod(new SingleLineAllCapsTransform(text.getContext())); + } + + private static class SingleLineAllCapsTransform extends SingleLineTransformationMethod { + private static final String TAG = "SingleLineAllCapsTransform"; + + private Locale mLocale; + + public SingleLineAllCapsTransform(Context context) { + mLocale = context.getResources().getConfiguration().locale; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + source = super.getTransformation(source, view); + return source != null ? source.toString().toUpperCase(mLocale) : null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java new file mode 100644 index 00000000000..ba8d0ec7c29 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java @@ -0,0 +1,85 @@ +/* + * 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. + */ + +package android.support.v4.view; + +/** + * Helper for accessing features in ScaleGestureDetector introduced + * after API level 19 (KitKat) in a backwards compatible fashion. + */ +public class ScaleGestureDetectorCompat { + static final ScaleGestureDetectorImpl IMPL; + + interface ScaleGestureDetectorImpl { + + public void setQuickScaleEnabled(Object o, boolean enabled); + + public boolean isQuickScaleEnabled(Object o); + } + + private static class BaseScaleGestureDetectorImpl implements ScaleGestureDetectorImpl { + @Override + public void setQuickScaleEnabled(Object o, boolean enabled) { + // Intentionally blank + } + + @Override + public boolean isQuickScaleEnabled(Object o) { + return false; + } + } + + private static class ScaleGestureDetectorCompatKitKatImpl implements ScaleGestureDetectorImpl { + @Override + public void setQuickScaleEnabled(Object o, boolean enabled) { + ScaleGestureDetectorCompatKitKat.setQuickScaleEnabled(o, enabled); + } + + @Override + public boolean isQuickScaleEnabled(Object o) { + return ScaleGestureDetectorCompatKitKat.isQuickScaleEnabled(o); + } + } + + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { // KitKat + IMPL = new ScaleGestureDetectorCompatKitKatImpl(); + } else { + IMPL = new BaseScaleGestureDetectorImpl(); + } + } + + private ScaleGestureDetectorCompat() {} + + /** + * Set whether the associated OnScaleGestureListener should receive onScale + * callbacks when the user performs a doubleTap followed by a swipe. Note that this is enabled + * by default if the app targets API 19 and newer. + * @param enabled true to enable quick scaling, false to disable + */ + public static void setQuickScaleEnabled(Object scaleGestureDetector, boolean enabled) { + IMPL.setQuickScaleEnabled(scaleGestureDetector, enabled); + } + + /** + * Return whether the quick scale gesture, in which the user performs a double tap followed by a + * swipe, should perform scaling. See setQuickScaleEnabled(Object, boolean). + */ + public static boolean isQuickScaleEnabled(Object scaleGestureDetector) { + return IMPL.isQuickScaleEnabled(scaleGestureDetector); // KitKat + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java new file mode 100644 index 00000000000..d3f8eb0452f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.ScaleGestureDetector; +import android.view.MotionEvent; +import android.content.Context; + +/** + * Implementation of ScaleGestureDetector compatibility that can call KitKat APIs. This class is an + * implementation detail for ScaleGestureDetectorCompat and should not be used directly. + */ +class ScaleGestureDetectorCompatKitKat { + + private ScaleGestureDetectorCompatKitKat() { + } + + public static void setQuickScaleEnabled(Object scaleGestureDetector, boolean enabled) { + ((ScaleGestureDetector) scaleGestureDetector).setQuickScaleEnabled(enabled); + } + + public static boolean isQuickScaleEnabled(Object scaleGestureDetector) { + return ((ScaleGestureDetector) scaleGestureDetector).isQuickScaleEnabled(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java new file mode 100644 index 00000000000..4fe0507917e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java @@ -0,0 +1,134 @@ +/* + * 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. + */ + +package android.support.v4.view; + +/** + * An interface that can be implemented by Views to provide scroll related APIs. + */ +public interface ScrollingView { + /** + *

    Compute the horizontal range that the horizontal scrollbar + * represents.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollExtent()} and + * {@link #computeHorizontalScrollOffset()}.

    + * + *

    The default range is the drawing width of this view.

    + * + * @return the total horizontal range represented by the horizontal + * scrollbar + * + * @see #computeHorizontalScrollExtent() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollRange(); + + /** + *

    Compute the horizontal offset of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollExtent()}.

    + * + *

    The default offset is the scroll offset of this view.

    + * + * @return the horizontal offset of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollOffset(); + + /** + *

    Compute the horizontal extent of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the length + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollOffset()}.

    + * + *

    The default extent is the drawing width of this view.

    + * + * @return the horizontal extent of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollExtent(); + + /** + *

    Compute the vertical range that the vertical scrollbar represents.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollExtent()} and + * {@link #computeVerticalScrollOffset()}.

    + * + * @return the total vertical range represented by the vertical scrollbar + * + *

    The default range is the drawing height of this view.

    + * + * @see #computeVerticalScrollExtent() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollRange(); + + /** + *

    Compute the vertical offset of the vertical scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollExtent()}.

    + * + *

    The default offset is the scroll offset of this view.

    + * + * @return the vertical offset of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollOffset(); + + /** + *

    Compute the vertical extent of the vertical scrollbar's thumb + * within the vertical range. This value is used to compute the length + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollOffset()}.

    + * + *

    The default extent is the drawing height of this view.

    + * + * @return the vertical extent of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollExtent(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java b/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java new file mode 100644 index 00000000000..83c014b756b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java @@ -0,0 +1,70 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.support.annotation.Nullable; + +/** + * Interface which allows a {@link android.view.View} to receive background tinting calls from + * {@code ViewCompat} when running on API v20 devices or lower. + */ +public interface TintableBackgroundView { + + /** + * Applies a tint to the background drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + *

    + * Subsequent calls to {@code View.setBackground(Drawable)} will automatically + * mutate the drawable and apply the specified tint and tint mode. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @see #getSupportBackgroundTintList() + */ + void setSupportBackgroundTintList(@Nullable ColorStateList tint); + + /** + * Return the tint applied to the background drawable, if specified. + * + * @return the tint applied to the background drawable + */ + @Nullable + ColorStateList getSupportBackgroundTintList(); + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setSupportBackgroundTintList(ColorStateList)}} to the background + * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @see #getSupportBackgroundTintMode() + */ + void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode); + + /** + * Return the blending mode used to apply the tint to the background + * drawable, if specified. + * + * @return the blending mode used to apply the tint to the background + * drawable + */ + @Nullable + PorterDuff.Mode getSupportBackgroundTintMode(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java new file mode 100644 index 00000000000..99286d076a0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.VelocityTracker; + +/** + * Helper for accessing features in {@link VelocityTracker} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class VelocityTrackerCompat { + /** + * Interface for the full API. + */ + interface VelocityTrackerVersionImpl { + public float getXVelocity(VelocityTracker tracker, int pointerId); + public float getYVelocity(VelocityTracker tracker, int pointerId); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseVelocityTrackerVersionImpl implements VelocityTrackerVersionImpl { + @Override + public float getXVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getXVelocity(); + } + @Override + public float getYVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getYVelocity(); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombVelocityTrackerVersionImpl implements VelocityTrackerVersionImpl { + @Override + public float getXVelocity(VelocityTracker tracker, int pointerId) { + return VelocityTrackerCompatHoneycomb.getXVelocity(tracker, pointerId); + } + @Override + public float getYVelocity(VelocityTracker tracker, int pointerId) { + return VelocityTrackerCompatHoneycomb.getYVelocity(tracker, pointerId); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final VelocityTrackerVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombVelocityTrackerVersionImpl(); + } else { + IMPL = new BaseVelocityTrackerVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Call {@link VelocityTracker#getXVelocity(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device, + * returns {@link VelocityTracker#getXVelocity()}. + */ + public static float getXVelocity(VelocityTracker tracker, int pointerId) { + return IMPL.getXVelocity(tracker, pointerId); + } + + /** + * Call {@link VelocityTracker#getYVelocity(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device, + * returns {@link VelocityTracker#getYVelocity()}. + */ + public static float getYVelocity(VelocityTracker tracker, int pointerId) { + return IMPL.getYVelocity(tracker, pointerId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java new file mode 100644 index 00000000000..4f9d326c982 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.VelocityTracker; + +/** + * Implementation of velocity tracker compatibility that can call Honeycomb APIs. + */ +class VelocityTrackerCompatHoneycomb { + public static float getXVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getXVelocity(pointerId); + } + public static float getYVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getYVelocity(pointerId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java new file mode 100644 index 00000000000..871f633335a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java @@ -0,0 +1,3008 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.IdRes; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.WeakHashMap; + +/** + * Helper for accessing features in {@link View} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class ViewCompat { + private static final String TAG = "ViewCompat"; + + + /** @hide */ + @IntDef({OVER_SCROLL_ALWAYS, OVER_SCROLL_IF_CONTENT_SCROLLS, OVER_SCROLL_NEVER}) + @Retention(RetentionPolicy.SOURCE) + private @interface OverScroll {} + + /** + * Always allow a user to over-scroll this view, provided it is a + * view that can scroll. + */ + public static final int OVER_SCROLL_ALWAYS = 0; + + /** + * Allow a user to over-scroll this view only if the content is large + * enough to meaningfully scroll, provided it is a view that can scroll. + */ + public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; + + /** + * Never allow a user to over-scroll this view. + */ + public static final int OVER_SCROLL_NEVER = 2; + + private static final long FAKE_FRAME_TIME = 10; + + /** @hide */ + @IntDef({ + IMPORTANT_FOR_ACCESSIBILITY_AUTO, + IMPORTANT_FOR_ACCESSIBILITY_YES, + IMPORTANT_FOR_ACCESSIBILITY_NO, + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + }) + @Retention(RetentionPolicy.SOURCE) + private @interface ImportantForAccessibility {} + + /** + * Automatically determine whether a view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0x00000000; + + /** + * The view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 0x00000001; + + /** + * The view is not important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 0x00000002; + + /** + * The view is not important for accessibility, nor are any of its + * descendant views. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004; + + /** @hide */ + @IntDef({ + ACCESSIBILITY_LIVE_REGION_NONE, + ACCESSIBILITY_LIVE_REGION_POLITE, + ACCESSIBILITY_LIVE_REGION_ASSERTIVE + }) + @Retention(RetentionPolicy.SOURCE) + private @interface AccessibilityLiveRegion {} + + /** + * Live region mode specifying that accessibility services should not + * automatically announce changes to this view. This is the default live + * region mode for most views. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000; + + /** + * Live region mode specifying that accessibility services should announce + * changes to this view. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001; + + /** + * Live region mode specifying that accessibility services should interrupt + * ongoing speech to immediately announce changes to this view. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 0x00000002; + + /** @hide */ + @IntDef({LAYER_TYPE_NONE, LAYER_TYPE_SOFTWARE, LAYER_TYPE_HARDWARE}) + @Retention(RetentionPolicy.SOURCE) + private @interface LayerType {} + + /** + * Indicates that the view does not have a layer. + */ + public static final int LAYER_TYPE_NONE = 0; + + /** + *

    Indicates that the view has a software layer. A software layer is backed + * by a bitmap and causes the view to be rendered using Android's software + * rendering pipeline, even if hardware acceleration is enabled.

    + * + *

    Software layers have various usages:

    + *

    When the application is not using hardware acceleration, a software layer + * is useful to apply a specific color filter and/or blending mode and/or + * translucency to a view and all its children.

    + *

    When the application is using hardware acceleration, a software layer + * is useful to render drawing primitives not supported by the hardware + * accelerated pipeline. It can also be used to cache a complex view tree + * into a texture and reduce the complexity of drawing operations. For instance, + * when animating a complex view tree with a translation, a software layer can + * be used to render the view tree only once.

    + *

    Software layers should be avoided when the affected view tree updates + * often. Every update will require to re-render the software layer, which can + * potentially be slow (particularly when hardware acceleration is turned on + * since the layer will have to be uploaded into a hardware texture after every + * update.)

    + */ + public static final int LAYER_TYPE_SOFTWARE = 1; + + /** + *

    Indicates that the view has a hardware layer. A hardware layer is backed + * by a hardware specific texture (generally Frame Buffer Objects or FBO on + * OpenGL hardware) and causes the view to be rendered using Android's hardware + * rendering pipeline, but only if hardware acceleration is turned on for the + * view hierarchy. When hardware acceleration is turned off, hardware layers + * behave exactly as {@link #LAYER_TYPE_SOFTWARE software layers}.

    + * + *

    A hardware layer is useful to apply a specific color filter and/or + * blending mode and/or translucency to a view and all its children.

    + *

    A hardware layer can be used to cache a complex view tree into a + * texture and reduce the complexity of drawing operations. For instance, + * when animating a complex view tree with a translation, a hardware layer can + * be used to render the view tree only once.

    + *

    A hardware layer can also be used to increase the rendering quality when + * rotation transformations are applied on a view. It can also be used to + * prevent potential clipping issues when applying 3D transforms on a view.

    + */ + public static final int LAYER_TYPE_HARDWARE = 2; + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, + LAYOUT_DIRECTION_INHERIT, + LAYOUT_DIRECTION_LOCALE}) + @Retention(RetentionPolicy.SOURCE) + private @interface LayoutDirectionMode {} + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }) + @Retention(RetentionPolicy.SOURCE) + private @interface ResolvedLayoutDirectionMode {} + + /** + * Horizontal layout direction of this view is from Left to Right. + */ + public static final int LAYOUT_DIRECTION_LTR = 0; + + /** + * Horizontal layout direction of this view is from Right to Left. + */ + public static final int LAYOUT_DIRECTION_RTL = 1; + + /** + * Horizontal layout direction of this view is inherited from its parent. + * Use with {@link #setLayoutDirection}. + */ + public static final int LAYOUT_DIRECTION_INHERIT = 2; + + /** + * Horizontal layout direction of this view is from deduced from the default language + * script for the locale. Use with {@link #setLayoutDirection}. + */ + public static final int LAYOUT_DIRECTION_LOCALE = 3; + + /** + * Bits of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that provide the actual measured size. + */ + public static final int MEASURED_SIZE_MASK = 0x00ffffff; + + /** + * Bits of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that provide the additional state bits. + */ + public static final int MEASURED_STATE_MASK = 0xff000000; + + /** + * Bit shift of {@link #MEASURED_STATE_MASK} to get to the height bits + * for functions that combine both width and height into a single int, + * such as {@link #getMeasuredState} and the childState argument of + * {@link #resolveSizeAndState(int, int, int)}. + */ + public static final int MEASURED_HEIGHT_STATE_SHIFT = 16; + + /** + * Bit of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that indicates the measured size + * is smaller that the space the view would like to have. + */ + public static final int MEASURED_STATE_TOO_SMALL = 0x01000000; + + /** + * Indicates no axis of view scrolling. + */ + public static final int SCROLL_AXIS_NONE = 0; + + /** + * Indicates scrolling along the horizontal axis. + */ + public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0; + + /** + * Indicates scrolling along the vertical axis. + */ + public static final int SCROLL_AXIS_VERTICAL = 1 << 1; + + interface ViewCompatImpl { + public boolean canScrollHorizontally(View v, int direction); + public boolean canScrollVertically(View v, int direction); + public int getOverScrollMode(View v); + public void setOverScrollMode(View v, int mode); + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event); + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info); + public void setAccessibilityDelegate(View v, @Nullable AccessibilityDelegateCompat delegate); + public boolean hasAccessibilityDelegate(View v); + public boolean hasTransientState(View view); + public void setHasTransientState(View view, boolean hasTransientState); + public void postInvalidateOnAnimation(View view); + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom); + public void postOnAnimation(View view, Runnable action); + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis); + public int getImportantForAccessibility(View view); + public void setImportantForAccessibility(View view, int mode); + public boolean isImportantForAccessibility(View view); + public boolean performAccessibilityAction(View view, int action, Bundle arguments); + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view); + public float getAlpha(View view); + public void setLayerType(View view, int layerType, Paint paint); + public int getLayerType(View view); + public int getLabelFor(View view); + public void setLabelFor(View view, int id); + public void setLayerPaint(View view, Paint paint); + public int getLayoutDirection(View view); + public void setLayoutDirection(View view, int layoutDirection); + public ViewParent getParentForAccessibility(View view); + public boolean isOpaque(View view); + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState); + public int getMeasuredWidthAndState(View view); + public int getMeasuredHeightAndState(View view); + public int getMeasuredState(View view); + public int getAccessibilityLiveRegion(View view); + public void setAccessibilityLiveRegion(View view, int mode); + public int getPaddingStart(View view); + public int getPaddingEnd(View view); + public void setPaddingRelative(View view, int start, int top, int end, int bottom); + public void dispatchStartTemporaryDetach(View view); + public void dispatchFinishTemporaryDetach(View view); + public float getX(View view); + public float getY(View view); + public float getRotation(View view); + public float getRotationX(View view); + public float getRotationY(View view); + public float getScaleX(View view); + public float getScaleY(View view); + public float getTranslationX(View view); + public float getTranslationY(View view); + public int getMinimumWidth(View view); + public int getMinimumHeight(View view); + public ViewPropertyAnimatorCompat animate(View view); + public void setRotation(View view, float value); + public void setRotationX(View view, float value); + public void setRotationY(View view, float value); + public void setScaleX(View view, float value); + public void setScaleY(View view, float value); + public void setTranslationX(View view, float value); + public void setTranslationY(View view, float value); + public void setX(View view, float value); + public void setY(View view, float value); + public void setAlpha(View view, float value); + public void setPivotX(View view, float value); + public void setPivotY(View view, float value); + public float getPivotX(View view); + public float getPivotY(View view); + public void setElevation(View view, float elevation); + public float getElevation(View view); + public void setTranslationZ(View view, float translationZ); + public float getTranslationZ(View view); + public void setTransitionName(View view, String transitionName); + public String getTransitionName(View view); + public int getWindowSystemUiVisibility(View view); + public void requestApplyInsets(View view); + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled); + public boolean getFitsSystemWindows(View view); + void setFitsSystemWindows(View view, boolean fitSystemWindows); + void jumpDrawablesToCurrentState(View v); + void setOnApplyWindowInsetsListener(View view, OnApplyWindowInsetsListener listener); + WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets); + WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets); + void setSaveFromParentEnabled(View view, boolean enabled); + void setActivated(View view, boolean activated); + boolean isPaddingRelative(View view); + ColorStateList getBackgroundTintList(View view); + void setBackgroundTintList(View view, ColorStateList tintList); + PorterDuff.Mode getBackgroundTintMode(View view); + void setBackgroundTintMode(View view, PorterDuff.Mode mode); + void setNestedScrollingEnabled(View view, boolean enabled); + boolean isNestedScrollingEnabled(View view); + boolean startNestedScroll(View view, int axes); + void stopNestedScroll(View view); + boolean hasNestedScrollingParent(View view); + boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow); + boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow); + boolean dispatchNestedFling(View view, float velocityX, float velocityY, boolean consumed); + boolean dispatchNestedPreFling(View view, float velocityX, float velocityY); + boolean isLaidOut(View view); + int combineMeasuredStates(int curState, int newState); + public float getZ(View view); + public boolean isAttachedToWindow(View view); + } + + static class BaseViewCompatImpl implements ViewCompatImpl { + private Method mDispatchStartTemporaryDetach; + private Method mDispatchFinishTemporaryDetach; + private boolean mTempDetachBound; + WeakHashMap mViewPropertyAnimatorCompatMap = null; + + + public boolean canScrollHorizontally(View v, int direction) { + return (v instanceof ScrollingView) && + canScrollingViewScrollHorizontally((ScrollingView) v, direction); + } + public boolean canScrollVertically(View v, int direction) { + return (v instanceof ScrollingView) && + canScrollingViewScrollVertically((ScrollingView) v, direction); + } + public int getOverScrollMode(View v) { + return OVER_SCROLL_NEVER; + } + public void setOverScrollMode(View v, int mode) { + // Do nothing; API doesn't exist + } + public void setAccessibilityDelegate(View v, AccessibilityDelegateCompat delegate) { + // Do nothing; API doesn't exist + } + + @Override + public boolean hasAccessibilityDelegate(View v) { + return false; + } + + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + // Do nothing; API doesn't exist + } + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + // Do nothing; API doesn't exist + } + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + // Do nothing; API doesn't exist + } + public boolean hasTransientState(View view) { + // A view can't have transient state if transient state wasn't supported. + return false; + } + public void setHasTransientState(View view, boolean hasTransientState) { + // Do nothing; API doesn't exist + } + public void postInvalidateOnAnimation(View view) { + view.invalidate(); + } + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom) { + view.invalidate(left, top, right, bottom); + } + public void postOnAnimation(View view, Runnable action) { + view.postDelayed(action, getFrameTime()); + } + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + view.postDelayed(action, getFrameTime() + delayMillis); + } + long getFrameTime() { + return FAKE_FRAME_TIME; + } + public int getImportantForAccessibility(View view) { + return 0; + } + public void setImportantForAccessibility(View view, int mode) { + + } + public boolean isImportantForAccessibility(View view) { + return true; + } + public boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return false; + } + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + return null; + } + public float getAlpha(View view) { + return 1.0f; + } + public void setLayerType(View view, int layerType, Paint paint) { + // No-op until layers became available (HC) + } + public int getLayerType(View view) { + return LAYER_TYPE_NONE; + } + public int getLabelFor(View view) { + return 0; + } + public void setLabelFor(View view, int id) { + + } + public void setLayerPaint(View view, Paint p) { + // No-op until layers became available (HC) + } + + @Override + public int getLayoutDirection(View view) { + return LAYOUT_DIRECTION_LTR; + } + + @Override + public void setLayoutDirection(View view, int layoutDirection) { + // No-op + } + + @Override + public ViewParent getParentForAccessibility(View view) { + return view.getParent(); + } + + @Override + public boolean isOpaque(View view) { + final Drawable bg = view.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return View.resolveSize(size, measureSpec); + } + + @Override + public int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidth(); + } + + @Override + public int getMeasuredHeightAndState(View view) { + return view.getMeasuredHeight(); + } + + @Override + public int getMeasuredState(View view) { + return 0; + } + + @Override + public int getAccessibilityLiveRegion(View view) { + return ACCESSIBILITY_LIVE_REGION_NONE; + } + + @Override + public void setAccessibilityLiveRegion(View view, int mode) { + // No-op + } + + @Override + public int getPaddingStart(View view) { + return view.getPaddingLeft(); + } + + @Override + public int getPaddingEnd(View view) { + return view.getPaddingRight(); + } + + @Override + public void setPaddingRelative(View view, int start, int top, int end, int bottom) { + view.setPadding(start, top, end, bottom); + } + + @Override + public void dispatchStartTemporaryDetach(View view) { + if (!mTempDetachBound) { + bindTempDetach(); + } + if (mDispatchStartTemporaryDetach != null) { + try { + mDispatchStartTemporaryDetach.invoke(view); + } catch (Exception e) { + Log.d(TAG, "Error calling dispatchStartTemporaryDetach", e); + } + } else { + // Try this instead + view.onStartTemporaryDetach(); + } + } + + @Override + public void dispatchFinishTemporaryDetach(View view) { + if (!mTempDetachBound) { + bindTempDetach(); + } + if (mDispatchFinishTemporaryDetach != null) { + try { + mDispatchFinishTemporaryDetach.invoke(view); + } catch (Exception e) { + Log.d(TAG, "Error calling dispatchFinishTemporaryDetach", e); + } + } else { + // Try this instead + view.onFinishTemporaryDetach(); + } + } + + private void bindTempDetach() { + try { + mDispatchStartTemporaryDetach = View.class.getDeclaredMethod( + "dispatchStartTemporaryDetach"); + mDispatchFinishTemporaryDetach = View.class.getDeclaredMethod( + "dispatchFinishTemporaryDetach"); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't find method", e); + } + mTempDetachBound = true; + } + + @Override + public float getTranslationX(View view) { + return 0; + } + + @Override + public float getTranslationY(View view) { + return 0; + } + + @Override + public float getX(View view) { + return 0; + } + + @Override + public float getY(View view) { + return 0; + } + + @Override + public float getRotation(View view) { + return 0; + } + + @Override + public float getRotationX(View view) { + return 0; + } + + @Override + public float getRotationY(View view) { + return 0; + } + + @Override + public float getScaleX(View view) { + return 0; + } + + @Override + public float getScaleY(View view) { + return 0; + } + + @Override + public int getMinimumWidth(View view) { + return ViewCompatBase.getMinimumWidth(view); + } + + @Override + public int getMinimumHeight(View view) { + return ViewCompatBase.getMinimumHeight(view); + } + + @Override + public ViewPropertyAnimatorCompat animate(View view) { + return new ViewPropertyAnimatorCompat(view); + } + + @Override + public void setRotation(View view, float value) { + // noop + } + + @Override + public void setTranslationX(View view, float value) { + // noop + } + + @Override + public void setTranslationY(View view, float value) { + // noop + } + + @Override + public void setAlpha(View view, float value) { + // noop + } + + @Override + public void setRotationX(View view, float value) { + // noop + } + + @Override + public void setRotationY(View view, float value) { + // noop + } + + @Override + public void setScaleX(View view, float value) { + // noop + } + + @Override + public void setScaleY(View view, float value) { + // noop + } + + @Override + public void setX(View view, float value) { + // noop + } + + @Override + public void setY(View view, float value) { + // noop + } + + @Override + public void setPivotX(View view, float value) { + // noop + } + + @Override + public void setPivotY(View view, float value) { + // noop + } + + @Override + public float getPivotX(View view) { + return 0; + } + + @Override + public float getPivotY(View view) { + return 0; + } + + @Override + public void setTransitionName(View view, String transitionName) { + } + + @Override + public String getTransitionName(View view) { + return null; + } + + @Override + public int getWindowSystemUiVisibility(View view) { + return 0; + } + + @Override + public void requestApplyInsets(View view) { + } + + @Override + public void setElevation(View view, float elevation) { + } + + @Override + public float getElevation(View view) { + return 0f; + } + + @Override + public void setTranslationZ(View view, float translationZ) { + } + + @Override + public float getTranslationZ(View view) { + return 0f; + } + + @Override + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + // noop + } + + @Override + public boolean getFitsSystemWindows(View view) { + return false; + } + + @Override + public void setFitsSystemWindows(View view, boolean fitSystemWindows) { + // noop + } + + @Override + public void jumpDrawablesToCurrentState(View view) { + // Do nothing; API didn't exist. + } + + @Override + public void setOnApplyWindowInsetsListener(View view, + OnApplyWindowInsetsListener listener) { + // noop + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + return insets; + } + + @Override + public WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + return insets; + } + + @Override + public void setSaveFromParentEnabled(View v, boolean enabled) { + // noop + } + + @Override + public void setActivated(View view, boolean activated) { + // noop + } + + @Override + public boolean isPaddingRelative(View view) { + return false; + } + + public void setNestedScrollingEnabled(View view, boolean enabled) { + if (view instanceof NestedScrollingChild) { + ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled); + } + } + + @Override + public boolean isNestedScrollingEnabled(View view) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).isNestedScrollingEnabled(); + } + return false; + } + + @Override + public ColorStateList getBackgroundTintList(View view) { + return ViewCompatBase.getBackgroundTintList(view); + } + + @Override + public void setBackgroundTintList(View view, ColorStateList tintList) { + ViewCompatBase.setBackgroundTintList(view, tintList); + } + + @Override + public void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + ViewCompatBase.setBackgroundTintMode(view, mode); + } + + @Override + public PorterDuff.Mode getBackgroundTintMode(View view) { + return ViewCompatBase.getBackgroundTintMode(view); + } + + private boolean canScrollingViewScrollHorizontally(ScrollingView view, int direction) { + final int offset = view.computeHorizontalScrollOffset(); + final int range = view.computeHorizontalScrollRange() - + view.computeHorizontalScrollExtent(); + if (range == 0) return false; + if (direction < 0) { + return offset > 0; + } else { + return offset < range - 1; + } + } + + private boolean canScrollingViewScrollVertically(ScrollingView view, int direction) { + final int offset = view.computeVerticalScrollOffset(); + final int range = view.computeVerticalScrollRange() - + view.computeVerticalScrollExtent(); + if (range == 0) return false; + if (direction < 0) { + return offset > 0; + } else { + return offset < range - 1; + } + } + + public boolean startNestedScroll(View view, int axes) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).startNestedScroll(axes); + } + return false; + } + + @Override + public void stopNestedScroll(View view) { + if (view instanceof NestedScrollingChild) { + ((NestedScrollingChild) view).stopNestedScroll(); + } + } + + @Override + public boolean hasNestedScrollingParent(View view) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).hasNestedScrollingParent(); + } + return false; + } + + @Override + public boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + return false; + } + + @Override + public boolean dispatchNestedPreScroll(View view, int dx, int dy, + int[] consumed, int[] offsetInWindow) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedPreScroll(dx, dy, consumed, + offsetInWindow); + } + return false; + } + + @Override + public boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedFling(velocityX, velocityY, + consumed); + } + return false; + } + + @Override + public boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedPreFling(velocityX, velocityY); + } + return false; + } + + @Override + public boolean isLaidOut(View view) { + return ViewCompatBase.isLaidOut(view); + } + + @Override + public int combineMeasuredStates(int curState, int newState) { + return curState | newState; + } + + @Override + public float getZ(View view) { + return getTranslationZ(view) + getElevation(view); + } + + @Override + public boolean isAttachedToWindow(View view) { + return ViewCompatBase.isAttachedToWindow(view); + } + } + + static class EclairMr1ViewCompatImpl extends BaseViewCompatImpl { + @Override + public boolean isOpaque(View view) { + return ViewCompatEclairMr1.isOpaque(view); + } + + @Override + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + ViewCompatEclairMr1.setChildrenDrawingOrderEnabled(viewGroup, enabled); + } + } + + static class GBViewCompatImpl extends EclairMr1ViewCompatImpl { + @Override + public int getOverScrollMode(View v) { + return ViewCompatGingerbread.getOverScrollMode(v); + } + @Override + public void setOverScrollMode(View v, int mode) { + ViewCompatGingerbread.setOverScrollMode(v, mode); + } + } + + static class HCViewCompatImpl extends GBViewCompatImpl { + @Override + long getFrameTime() { + return ViewCompatHC.getFrameTime(); + } + @Override + public float getAlpha(View view) { + return ViewCompatHC.getAlpha(view); + } + @Override + public void setLayerType(View view, int layerType, Paint paint) { + ViewCompatHC.setLayerType(view, layerType, paint); + } + @Override + public int getLayerType(View view) { + return ViewCompatHC.getLayerType(view); + } + @Override + public void setLayerPaint(View view, Paint paint) { + // Make sure the paint is correct; this will be cheap if it's the same + // instance as was used to call setLayerType earlier. + setLayerType(view, getLayerType(view), paint); + // This is expensive, but the only way to accomplish this before JB-MR1. + view.invalidate(); + } + @Override + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return ViewCompatHC.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + @Override + public int getMeasuredWidthAndState(View view) { + return ViewCompatHC.getMeasuredWidthAndState(view); + } + @Override + public int getMeasuredHeightAndState(View view) { + return ViewCompatHC.getMeasuredHeightAndState(view); + } + @Override + public int getMeasuredState(View view) { + return ViewCompatHC.getMeasuredState(view); + } + @Override + public float getTranslationX(View view) { + return ViewCompatHC.getTranslationX(view); + } + @Override + public float getTranslationY(View view) { + return ViewCompatHC.getTranslationY(view); + } + @Override + public void setTranslationX(View view, float value) { + ViewCompatHC.setTranslationX(view, value); + } + @Override + public void setTranslationY(View view, float value) { + ViewCompatHC.setTranslationY(view, value); + } + @Override + public void setAlpha(View view, float value) { + ViewCompatHC.setAlpha(view, value); + } + @Override + public void setX(View view, float value) { + ViewCompatHC.setX(view, value); + } + @Override + public void setY(View view, float value) { + ViewCompatHC.setY(view, value); + } + @Override + public void setRotation(View view, float value) { + ViewCompatHC.setRotation(view, value); + } + @Override + public void setRotationX(View view, float value) { + ViewCompatHC.setRotationX(view, value); + } + @Override + public void setRotationY(View view, float value) { + ViewCompatHC.setRotationY(view, value); + } + @Override + public void setScaleX(View view, float value) { + ViewCompatHC.setScaleX(view, value); + } + @Override + public void setScaleY(View view, float value) { + ViewCompatHC.setScaleY(view, value); + } + @Override + public void setPivotX(View view, float value) { + ViewCompatHC.setPivotX(view, value); + } + @Override + public void setPivotY(View view, float value) { + ViewCompatHC.setPivotY(view, value); + } + @Override + public float getX(View view) { + return ViewCompatHC.getX(view); + } + + @Override + public float getY(View view) { + return ViewCompatHC.getY(view); + } + + @Override + public float getRotation(View view) { + return ViewCompatHC.getRotation(view); + } + + @Override + public float getRotationX(View view) { + return ViewCompatHC.getRotationX(view); + } + + @Override + public float getRotationY(View view) { + return ViewCompatHC.getRotationY(view); + } + + @Override + public float getScaleX(View view) { + return ViewCompatHC.getScaleX(view); + } + + @Override + public float getScaleY(View view) { + return ViewCompatHC.getScaleY(view); + } + + @Override + public float getPivotX(View view) { + return ViewCompatHC.getPivotX(view); + } + @Override + public float getPivotY(View view) { + return ViewCompatHC.getPivotY(view); + } + @Override + public void jumpDrawablesToCurrentState(View view) { + ViewCompatHC.jumpDrawablesToCurrentState(view); + } + + @Override + public void setSaveFromParentEnabled(View view, boolean enabled) { + ViewCompatHC.setSaveFromParentEnabled(view, enabled); + } + + @Override + public void setActivated(View view, boolean activated) { + ViewCompatHC.setActivated(view, activated); + } + + @Override + public int combineMeasuredStates(int curState, int newState) { + return ViewCompatHC.combineMeasuredStates(curState, newState); + } + } + + static class ICSViewCompatImpl extends HCViewCompatImpl { + static Field mAccessibilityDelegateField; + static boolean accessibilityDelegateCheckFailed = false; + @Override + public boolean canScrollHorizontally(View v, int direction) { + return ViewCompatICS.canScrollHorizontally(v, direction); + } + @Override + public boolean canScrollVertically(View v, int direction) { + return ViewCompatICS.canScrollVertically(v, direction); + } + @Override + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + ViewCompatICS.onPopulateAccessibilityEvent(v, event); + } + @Override + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + ViewCompatICS.onInitializeAccessibilityEvent(v, event); + } + @Override + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + ViewCompatICS.onInitializeAccessibilityNodeInfo(v, info.getInfo()); + } + @Override + public void setAccessibilityDelegate(View v, + @Nullable AccessibilityDelegateCompat delegate) { + ViewCompatICS.setAccessibilityDelegate(v, + delegate == null ? null : delegate.getBridge()); + } + + @Override + public boolean hasAccessibilityDelegate(View v) { + if (accessibilityDelegateCheckFailed) { + return false; // View implementation might have changed. + } + if (mAccessibilityDelegateField == null) { + try { + mAccessibilityDelegateField = View.class + .getDeclaredField("mAccessibilityDelegate"); + mAccessibilityDelegateField.setAccessible(true); + } catch (Throwable t) { + accessibilityDelegateCheckFailed = true; + return false; + } + } + try { + return mAccessibilityDelegateField.get(v) != null; + } catch (Throwable t) { + accessibilityDelegateCheckFailed = true; + return false; + } + } + + @Override + public ViewPropertyAnimatorCompat animate(View view) { + if (mViewPropertyAnimatorCompatMap == null) { + mViewPropertyAnimatorCompatMap = + new WeakHashMap(); + } + ViewPropertyAnimatorCompat vpa = mViewPropertyAnimatorCompatMap.get(view); + if (vpa == null) { + vpa = new ViewPropertyAnimatorCompat(view); + mViewPropertyAnimatorCompatMap.put(view, vpa); + } + return vpa; + } + + @Override + public void setFitsSystemWindows(View view, boolean fitSystemWindows) { + ViewCompatICS.setFitsSystemWindows(view, fitSystemWindows); + } + } + + static class JBViewCompatImpl extends ICSViewCompatImpl { + @Override + public boolean hasTransientState(View view) { + return ViewCompatJB.hasTransientState(view); + } + @Override + public void setHasTransientState(View view, boolean hasTransientState) { + ViewCompatJB.setHasTransientState(view, hasTransientState); + } + @Override + public void postInvalidateOnAnimation(View view) { + ViewCompatJB.postInvalidateOnAnimation(view); + } + @Override + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom) { + ViewCompatJB.postInvalidateOnAnimation(view, left, top, right, bottom); + } + @Override + public void postOnAnimation(View view, Runnable action) { + ViewCompatJB.postOnAnimation(view, action); + } + @Override + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + ViewCompatJB.postOnAnimationDelayed(view, action, delayMillis); + } + @Override + public int getImportantForAccessibility(View view) { + return ViewCompatJB.getImportantForAccessibility(view); + } + @Override + public void setImportantForAccessibility(View view, int mode) { + // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS is not available + // on this platform so replace with IMPORTANT_FOR_ACCESSIBILITY_NO + // which is closer semantically. + if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { + mode = IMPORTANT_FOR_ACCESSIBILITY_NO; + } + ViewCompatJB.setImportantForAccessibility(view, mode); + } + @Override + public boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return ViewCompatJB.performAccessibilityAction(view, action, arguments); + } + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + Object compat = ViewCompatJB.getAccessibilityNodeProvider(view); + if (compat != null) { + return new AccessibilityNodeProviderCompat(compat); + } + return null; + } + + @Override + public ViewParent getParentForAccessibility(View view) { + return ViewCompatJB.getParentForAccessibility(view); + } + + @Override + public int getMinimumWidth(View view) { + return ViewCompatJB.getMinimumWidth(view); + } + + @Override + public int getMinimumHeight(View view) { + return ViewCompatJB.getMinimumHeight(view); + } + + @Override + public void requestApplyInsets(View view) { + ViewCompatJB.requestApplyInsets(view); + } + + @Override + public boolean getFitsSystemWindows(View view) { + return ViewCompatJB.getFitsSystemWindows(view); + } + } + + static class JbMr1ViewCompatImpl extends JBViewCompatImpl { + + @Override + public int getLabelFor(View view) { + return ViewCompatJellybeanMr1.getLabelFor(view); + } + + @Override + public void setLabelFor(View view, int id) { + ViewCompatJellybeanMr1.setLabelFor(view, id); + } + + @Override + public void setLayerPaint(View view, Paint paint) { + ViewCompatJellybeanMr1.setLayerPaint(view, paint); + } + + @Override + public int getLayoutDirection(View view) { + return ViewCompatJellybeanMr1.getLayoutDirection(view); + } + + @Override + public void setLayoutDirection(View view, int layoutDirection) { + ViewCompatJellybeanMr1.setLayoutDirection(view, layoutDirection); + } + + @Override + public int getPaddingStart(View view) { + return ViewCompatJellybeanMr1.getPaddingStart(view); + } + + @Override + public int getPaddingEnd(View view) { + return ViewCompatJellybeanMr1.getPaddingEnd(view); + } + + @Override + public void setPaddingRelative(View view, int start, int top, int end, int bottom) { + ViewCompatJellybeanMr1.setPaddingRelative(view, start, top, end, bottom); + } + + @Override + public int getWindowSystemUiVisibility(View view) { + return ViewCompatJellybeanMr1.getWindowSystemUiVisibility(view); + } + + @Override + public boolean isPaddingRelative(View view) { + return ViewCompatJellybeanMr1.isPaddingRelative(view); + } + } + + static class KitKatViewCompatImpl extends JbMr1ViewCompatImpl { + @Override + public int getAccessibilityLiveRegion(View view) { + return ViewCompatKitKat.getAccessibilityLiveRegion(view); + } + + @Override + public void setAccessibilityLiveRegion(View view, int mode) { + ViewCompatKitKat.setAccessibilityLiveRegion(view, mode); + } + + @Override + public void setImportantForAccessibility(View view, int mode) { + ViewCompatJB.setImportantForAccessibility(view, mode); + } + + @Override + public boolean isLaidOut(View view) { + return ViewCompatKitKat.isLaidOut(view); + } + + @Override + public boolean isAttachedToWindow(View view) { + return ViewCompatKitKat.isAttachedToWindow(view); + } + } + + static class LollipopViewCompatImpl extends KitKatViewCompatImpl { + @Override + public void setTransitionName(View view, String transitionName) { + ViewCompatLollipop.setTransitionName(view, transitionName); + } + + @Override + public String getTransitionName(View view) { + return ViewCompatLollipop.getTransitionName(view); + } + + @Override + public void requestApplyInsets(View view) { + ViewCompatLollipop.requestApplyInsets(view); + } + + @Override + public void setElevation(View view, float elevation) { + ViewCompatLollipop.setElevation(view, elevation); + } + + @Override + public float getElevation(View view) { + return ViewCompatLollipop.getElevation(view); + } + + @Override + public void setTranslationZ(View view, float translationZ) { + ViewCompatLollipop.setTranslationZ(view, translationZ); + } + + @Override + public float getTranslationZ(View view) { + return ViewCompatLollipop.getTranslationZ(view); + } + + @Override + public void setOnApplyWindowInsetsListener(View view, OnApplyWindowInsetsListener listener) { + ViewCompatLollipop.setOnApplyWindowInsetsListener(view, listener); + } + + @Override + public void setNestedScrollingEnabled(View view, boolean enabled) { + ViewCompatLollipop.setNestedScrollingEnabled(view, enabled); + } + + @Override + public boolean isNestedScrollingEnabled(View view) { + return ViewCompatLollipop.isNestedScrollingEnabled(view); + } + + @Override + public boolean startNestedScroll(View view, int axes) { + return ViewCompatLollipop.startNestedScroll(view, axes); + } + + @Override + public void stopNestedScroll(View view) { + ViewCompatLollipop.stopNestedScroll(view); + } + + @Override + public boolean hasNestedScrollingParent(View view) { + return ViewCompatLollipop.hasNestedScrollingParent(view); + } + + @Override + public boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return ViewCompatLollipop.dispatchNestedScroll(view, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(View view, int dx, int dy, + int[] consumed, int[] offsetInWindow) { + return ViewCompatLollipop.dispatchNestedPreScroll(view, dx, dy, consumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return ViewCompatLollipop.dispatchNestedFling(view, velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return ViewCompatLollipop.dispatchNestedPreFling(view, velocityX, velocityY); + } + + @Override + public boolean isImportantForAccessibility(View view) { + return ViewCompatLollipop.isImportantForAccessibility(view); + } + + @Override + public ColorStateList getBackgroundTintList(View view) { + return ViewCompatLollipop.getBackgroundTintList(view); + } + + @Override + public void setBackgroundTintList(View view, ColorStateList tintList) { + ViewCompatLollipop.setBackgroundTintList(view, tintList); + } + + @Override + public void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + ViewCompatLollipop.setBackgroundTintMode(view, mode); + } + + @Override + public PorterDuff.Mode getBackgroundTintMode(View view) { + return ViewCompatLollipop.getBackgroundTintMode(view); + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + return ViewCompatLollipop.onApplyWindowInsets(v, insets); + } + + @Override + public WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + return ViewCompatLollipop.dispatchApplyWindowInsets(v, insets); + } + + @Override + public float getZ(View view) { + return ViewCompatLollipop.getZ(view); + } + } + + static final ViewCompatImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LollipopViewCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1ViewCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewCompatImpl(); + } else if (version >= 11) { + IMPL = new HCViewCompatImpl(); + } else if (version >= 9) { + IMPL = new GBViewCompatImpl(); + } else if (version >= 7) { + IMPL = new EclairMr1ViewCompatImpl(); + } else { + IMPL = new BaseViewCompatImpl(); + } + } + + /** + * Check if this view can be scrolled horizontally in a certain direction. + * + * @param v The View against which to invoke the method. + * @param direction Negative to check scrolling left, positive to check scrolling right. + * @return true if this view can be scrolled in the specified direction, false otherwise. + */ + public static boolean canScrollHorizontally(View v, int direction) { + return IMPL.canScrollHorizontally(v, direction); + } + + /** + * Check if this view can be scrolled vertically in a certain direction. + * + * @param v The View against which to invoke the method. + * @param direction Negative to check scrolling up, positive to check scrolling down. + * @return true if this view can be scrolled in the specified direction, false otherwise. + */ + public static boolean canScrollVertically(View v, int direction) { + return IMPL.canScrollVertically(v, direction); + } + + /** + * Returns the over-scroll mode for this view. The result will be + * one of {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * @param v The View against which to invoke the method. + * @return This view's over-scroll mode. + */ + @OverScroll + public static int getOverScrollMode(View v) { + return IMPL.getOverScrollMode(v); + } + + /** + * Set the over-scroll mode for this view. Valid over-scroll modes are + * {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * Setting the over-scroll mode of a view will have an effect only if the + * view is capable of scrolling. + * + * @param v The View against which to invoke the method. + * @param overScrollMode The new over-scroll mode for this view. + */ + public static void setOverScrollMode(View v, @OverScroll int overScrollMode) { + IMPL.setOverScrollMode(v, overScrollMode); + } + + /** + * Called from {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * giving a chance to this View to populate the accessibility event with its + * text content. While this method is free to modify event + * attributes other than text content, doing so should normally be performed in + * {@link View#onInitializeAccessibilityEvent(AccessibilityEvent)}. + *

    + * Example: Adding formatted date string to an accessibility event in addition + * to the text added by the super implementation: + *

     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    +     *     super.onPopulateAccessibilityEvent(event);
    +     *     final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY;
    +     *     String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    +     *         mCurrentDate.getTimeInMillis(), flags);
    +     *     event.getText().add(selectedDateUtterance);
    +     * }
    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onPopulateAccessibilityEvent(View, + * AccessibilityEvent)} + * is responsible for handling this call. + *

    + *

    Note: Always call the super implementation before adding + * information to the event, in case the default implementation has basic information to add. + *

    + * + * @param v The View against which to invoke the method. + * @param event The accessibility event which to populate. + * + * @see View#sendAccessibilityEvent(int) + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public static void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + IMPL.onPopulateAccessibilityEvent(v, event); + } + + /** + * Initializes an {@link AccessibilityEvent} with information about + * this View which is the event source. In other words, the source of + * an accessibility event is the view whose state change triggered firing + * the event. + *

    + * Example: Setting the password property of an event in addition + * to properties set by the super implementation: + *

     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    +     *     super.onInitializeAccessibilityEvent(event);
    +     *     event.setPassword(true);
    +     * }
    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onInitializeAccessibilityEvent(View, + * AccessibilityEvent)} + * is responsible for handling this call. + *

    + *

    Note: Always call the super implementation before adding + * information to the event, in case the default implementation has basic information to add. + *

    + * + * @param v The View against which to invoke the method. + * @param event The event to initialize. + * + * @see View#sendAccessibilityEvent(int) + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public static void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + IMPL.onInitializeAccessibilityEvent(v, event); + } + + /** + * Initializes an {@link android.view.accessibility.AccessibilityNodeInfo} with information + * about this view. The base implementation sets: + *
      + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setParent(View)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent(Rect)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen(Rect)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setPackageName(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setClassName(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setContentDescription(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setEnabled(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setClickable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setFocusable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setFocused(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setLongClickable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setSelected(boolean)},
    • + *
    + *

    + * Subclasses should override this method, call the super implementation, + * and set additional attributes. + *

    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onInitializeAccessibilityNodeInfo(View, + * android.view.accessibility.AccessibilityNodeInfo)} + * is responsible for handling this call. + *

    + * + * @param v The View against which to invoke the method. + * @param info The instance to initialize. + */ + public static void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + IMPL.onInitializeAccessibilityNodeInfo(v, info); + } + + /** + * Sets a delegate for implementing accessibility support via compositon as + * opposed to inheritance. The delegate's primary use is for implementing + * backwards compatible widgets. For more details see + * {@link android.view.View.AccessibilityDelegate}. + * + * @param v The View against which to invoke the method. + * @param delegate The delegate instance. + * + * @see android.view.View.AccessibilityDelegate + */ + public static void setAccessibilityDelegate(View v, AccessibilityDelegateCompat delegate) { + IMPL.setAccessibilityDelegate(v, delegate); + } + + /** + * Checks whether provided View has an accessibility delegate attached to it. + * + * @param v The View instance to check + * @return True if the View has an accessibility delegate + */ + public static boolean hasAccessibilityDelegate(View v) { + return IMPL.hasAccessibilityDelegate(v); + } + + /** + * Indicates whether the view is currently tracking transient state that the + * app should not need to concern itself with saving and restoring, but that + * the framework should take special note to preserve when possible. + * + * @param view View to check for transient state + * @return true if the view has transient state + */ + public static boolean hasTransientState(View view) { + return IMPL.hasTransientState(view); + } + + /** + * Set whether this view is currently tracking transient state that the + * framework should attempt to preserve when possible. + * + * @param view View tracking transient state + * @param hasTransientState true if this view has transient state + */ + public static void setHasTransientState(View view, boolean hasTransientState) { + IMPL.setHasTransientState(view, hasTransientState); + } + + /** + *

    Cause an invalidate to happen on the next animation time step, typically the + * next display frame.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to invalidate + */ + public static void postInvalidateOnAnimation(View view) { + IMPL.postInvalidateOnAnimation(view); + } + + /** + *

    Cause an invalidate of the specified area to happen on the next animation + * time step, typically the next display frame.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to invalidate + * @param left The left coordinate of the rectangle to invalidate. + * @param top The top coordinate of the rectangle to invalidate. + * @param right The right coordinate of the rectangle to invalidate. + * @param bottom The bottom coordinate of the rectangle to invalidate. + */ + public static void postInvalidateOnAnimation(View view, int left, int top, + int right, int bottom) { + IMPL.postInvalidateOnAnimation(view, left, top, right, bottom); + } + + /** + *

    Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to post this Runnable to + * @param action The Runnable that will be executed. + */ + public static void postOnAnimation(View view, Runnable action) { + IMPL.postOnAnimation(view, action); + } + + /** + *

    Causes the Runnable to execute on the next animation time step, + * after the specified amount of time elapses. + * The runnable will be run on the user interface thread.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view The view to post this Runnable to + * @param action The Runnable that will be executed. + * @param delayMillis The delay (in milliseconds) until the Runnable + * will be executed. + */ + public static void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + IMPL.postOnAnimationDelayed(view, action, delayMillis); + } + + /** + * Gets the mode for determining whether this View is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @param view The view whose property to get. + * @return The mode for determining whether a View is important for accessibility. + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + @ImportantForAccessibility + public static int getImportantForAccessibility(View view) { + return IMPL.getImportantForAccessibility(view); + } + + /** + * Sets how to determine whether this view is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + *

    + * Note: If the current paltform version does not support the + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} mode, then + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO} will be used as it is the + * closest terms of semantics. + *

    + * + * @param view The view whose property to set. + * @param mode How to determine whether this view is important for accessibility. + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + public static void setImportantForAccessibility(View view, + @ImportantForAccessibility int mode) { + IMPL.setImportantForAccessibility(view, mode); + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfoCompat}. + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link #setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#performAccessibilityAction(View, int, Bundle)} + * is responsible for handling this call. + *

    + * + * @param action The action to perform. + * @param arguments Optional action arguments. + * @return Whether the action was performed. + */ + public static boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return IMPL.performAccessibilityAction(view, action, arguments); + } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + *

    + * If this method returns an instance, this instance is responsible for managing + * {@link AccessibilityNodeInfoCompat}s describing the virtual sub-tree rooted at + * this View including the one representing the View itself. Similarly the returned + * instance is responsible for performing accessibility actions on any virtual + * view or the root view itself. + *

    + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link #setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#getAccessibilityNodeProvider(View)} + * is responsible for handling this call. + *

    + * + * @param view The view whose property to get. + * @return The provider. + * + * @see AccessibilityNodeProviderCompat + */ + public static AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + return IMPL.getAccessibilityNodeProvider(view); + } + + /** + * The opacity of the view. This is a value from 0 to 1, where 0 means the view is + * completely transparent and 1 means the view is completely opaque. + * + *

    By default this is 1.0f. Prior to API 11, the returned value is always 1.0f. + * @return The opacity of the view. + */ + public static float getAlpha(View view) { + return IMPL.getAlpha(view); + } + + /** + *

    Specifies the type of layer backing this view. The layer can be + * {@link #LAYER_TYPE_NONE disabled}, {@link #LAYER_TYPE_SOFTWARE software} or + * {@link #LAYER_TYPE_HARDWARE hardware}.

    + * + *

    A layer is associated with an optional {@link android.graphics.Paint} + * instance that controls how the layer is composed on screen. The following + * properties of the paint are taken into account when composing the layer:

    + *
      + *
    • {@link android.graphics.Paint#getAlpha() Translucency (alpha)}
    • + *
    • {@link android.graphics.Paint#getXfermode() Blending mode}
    • + *
    • {@link android.graphics.Paint#getColorFilter() Color filter}
    • + *
    + * + *

    If this view has an alpha value set to < 1.0 by calling + * setAlpha(float), the alpha value of the layer's paint is replaced by + * this view's alpha value. Calling setAlpha(float) is therefore + * equivalent to setting a hardware layer on this view and providing a paint with + * the desired alpha value.

    + * + *

    Refer to the documentation of {@link #LAYER_TYPE_NONE disabled}, + * {@link #LAYER_TYPE_SOFTWARE software} and {@link #LAYER_TYPE_HARDWARE hardware} + * for more information on when and how to use layers.

    + * + * @param layerType The ype of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + * + * @param view View to set the layer type for + * @param layerType The type of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + */ + public static void setLayerType(View view, @LayerType int layerType, Paint paint) { + IMPL.setLayerType(view, layerType, paint); + } + + /** + * Indicates what type of layer is currently associated with this view. By default + * a view does not have a layer, and the layer type is {@link #LAYER_TYPE_NONE}. + * Refer to the documentation of + * {@link #setLayerType(android.view.View, int, android.graphics.Paint)} + * for more information on the different types of layers. + * + * @param view The view to fetch the layer type from + * @return {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * + * @see #setLayerType(android.view.View, int, android.graphics.Paint) + * @see #LAYER_TYPE_NONE + * @see #LAYER_TYPE_SOFTWARE + * @see #LAYER_TYPE_HARDWARE + */ + @LayerType + public static int getLayerType(View view) { + return IMPL.getLayerType(view); + } + + /** + * Gets the id of a view for which a given view serves as a label for + * accessibility purposes. + * + * @param view The view on which to invoke the corresponding method. + * @return The labeled view id. + */ + public static int getLabelFor(View view) { + return IMPL.getLabelFor(view); + } + + /** + * Sets the id of a view for which a given view serves as a label for + * accessibility purposes. + * + * @param view The view on which to invoke the corresponding method. + * @param labeledId The labeled view id. + */ + public static void setLabelFor(View view, @IdRes int labeledId) { + IMPL.setLabelFor(view, labeledId); + } + + /** + * Updates the {@link Paint} object used with the current layer (used only if the current + * layer type is not set to {@link #LAYER_TYPE_NONE}). Changed properties of the Paint + * provided to {@link #setLayerType(android.view.View, int, android.graphics.Paint)} + * will be used the next time the View is redrawn, but + * {@link #setLayerPaint(android.view.View, android.graphics.Paint)} + * must be called to ensure that the view gets redrawn immediately. + * + *

    A layer is associated with an optional {@link android.graphics.Paint} + * instance that controls how the layer is composed on screen. The following + * properties of the paint are taken into account when composing the layer:

    + *
      + *
    • {@link android.graphics.Paint#getAlpha() Translucency (alpha)}
    • + *
    • {@link android.graphics.Paint#getXfermode() Blending mode}
    • + *
    • {@link android.graphics.Paint#getColorFilter() Color filter}
    • + *
    + * + *

    If this view has an alpha value set to < 1.0 by calling + * View#setAlpha(float), the alpha value of the layer's paint is replaced by + * this view's alpha value. Calling View#setAlpha(float) is therefore + * equivalent to setting a hardware layer on this view and providing a paint with + * the desired alpha value.

    + * + * @param view View to set a layer paint for + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + * + * @see #setLayerType(View, int, android.graphics.Paint) + */ + public static void setLayerPaint(View view, Paint paint) { + IMPL.setLayerPaint(view, paint); + } + + /** + * Returns the resolved layout direction for this view. + * + * @param view View to get layout direction for + * @return {@link #LAYOUT_DIRECTION_RTL} if the layout direction is RTL or returns + * {@link #LAYOUT_DIRECTION_LTR} if the layout direction is not RTL. + * + * For compatibility, this will return {@link #LAYOUT_DIRECTION_LTR} if API version + * is lower than Jellybean MR1 (API 17) + */ + @ResolvedLayoutDirectionMode + public static int getLayoutDirection(View view) { + return IMPL.getLayoutDirection(view); + } + + /** + * Set the layout direction for this view. This will propagate a reset of layout direction + * resolution to the view's children and resolve layout direction for this view. + * + * @param view View to set layout direction for + * @param layoutDirection the layout direction to set. Should be one of: + * + * {@link #LAYOUT_DIRECTION_LTR}, + * {@link #LAYOUT_DIRECTION_RTL}, + * {@link #LAYOUT_DIRECTION_INHERIT}, + * {@link #LAYOUT_DIRECTION_LOCALE}. + * + * Resolution will be done if the value is set to LAYOUT_DIRECTION_INHERIT. The resolution + * proceeds up the parent chain of the view to get the value. If there is no parent, then it + * will return the default {@link #LAYOUT_DIRECTION_LTR}. + */ + public static void setLayoutDirection(View view, @LayoutDirectionMode int layoutDirection) { + IMPL.setLayoutDirection(view, layoutDirection); + } + + /** + * Gets the parent for accessibility purposes. Note that the parent for + * accessibility is not necessary the immediate parent. It is the first + * predecessor that is important for accessibility. + * + * @param view View to retrieve parent for + * @return The parent for use in accessibility inspection + */ + public static ViewParent getParentForAccessibility(View view) { + return IMPL.getParentForAccessibility(view); + } + + /** + * Indicates whether this View is opaque. An opaque View guarantees that it will + * draw all the pixels overlapping its bounds using a fully opaque color. + * + * On API 7 and above this will call View's true isOpaque method. On previous platform + * versions it will check the opacity of the view's background drawable if present. + * + * @return True if this View is guaranteed to be fully opaque, false otherwise. + */ + public static boolean isOpaque(View view) { + return IMPL.isOpaque(view); + } + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, + * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and + * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting + * size is smaller than the size the view wants to be. + * + * @param size How big the view wants to be + * @param measureSpec Constraints imposed by the parent + * @return Size information bit mask as defined by + * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. + */ + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return IMPL.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + + /** + * Return the full width measurement information for this view as computed + * by the most recent call to {@link android.view.View#measure(int, int)}. + * This result is a bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * This should be used during measurement and layout calculations only. Use + * {@link android.view.View#getWidth()} to see how wide a view is after layout. + * + * @return The measured width of this view as a bit mask. + */ + public static int getMeasuredWidthAndState(View view) { + return IMPL.getMeasuredWidthAndState(view); + } + + /** + * Return the full height measurement information for this view as computed + * by the most recent call to {@link android.view.View#measure(int, int)}. + * This result is a bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * This should be used during measurement and layout calculations only. Use + * {@link android.view.View#getHeight()} to see how wide a view is after layout. + * + * @return The measured width of this view as a bit mask. + */ + public static int getMeasuredHeightAndState(View view) { + return IMPL.getMeasuredHeightAndState(view); + } + + /** + * Return only the state bits of {@link #getMeasuredWidthAndState} + * and {@link #getMeasuredHeightAndState}, combined into one integer. + * The width component is in the regular bits {@link #MEASURED_STATE_MASK} + * and the height component is at the shifted bits + * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}. + */ + public static int getMeasuredState(View view) { + return IMPL.getMeasuredState(view); + } + + /** + * Merge two states as returned by {@link #getMeasuredState(View)}. + * @param curState The current state as returned from a view or the result + * of combining multiple views. + * @param newState The new view state to combine. + * @return Returns a new integer reflecting the combination of the two + * states. + */ + public static int combineMeasuredStates(int curState, int newState) { + return IMPL.combineMeasuredStates(curState, newState); + } + + /** + * Gets the live region mode for the specified View. + * + * @param view The view from which to obtain the live region mode + * @return The live region mode for the view. + * + * @see ViewCompat#setAccessibilityLiveRegion(View, int) + */ + @AccessibilityLiveRegion + public static int getAccessibilityLiveRegion(View view) { + return IMPL.getAccessibilityLiveRegion(view); + } + + /** + * Sets the live region mode for the specified view. This indicates to + * accessibility services whether they should automatically notify the user + * about changes to the view's content description or text, or to the + * content descriptions or text of the view's children (where applicable). + *

    + * For example, in a login screen with a TextView that displays an "incorrect + * password" notification, that view should be marked as a live region with + * mode {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * To disable change notifications for this view, use + * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region + * mode for most views. + *

    + * To indicate that the user should be notified of changes, use + * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * If the view's changes should interrupt ongoing speech and notify the user + * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. + * + * @param view The view on which to set the live region mode + * @param mode The live region mode for this view, one of: + *

      + *
    • {@link #ACCESSIBILITY_LIVE_REGION_NONE} + *
    • {@link #ACCESSIBILITY_LIVE_REGION_POLITE} + *
    • {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE} + *
    + */ + public static void setAccessibilityLiveRegion(View view, @AccessibilityLiveRegion int mode) { + IMPL.setAccessibilityLiveRegion(view, mode); + } + + /** + * Returns the start padding of the specified view depending on its resolved layout direction. + * If there are inset and enabled scrollbars, this value may include the space + * required to display the scrollbars as well. + * + * @param view The view to get padding for + * @return the start padding in pixels + */ + public static int getPaddingStart(View view) { + return IMPL.getPaddingStart(view); + } + + /** + * Returns the end padding of the specified view depending on its resolved layout direction. + * If there are inset and enabled scrollbars, this value may include the space + * required to display the scrollbars as well. + * + * @param view The view to get padding for + * @return the end padding in pixels + */ + public static int getPaddingEnd(View view) { + return IMPL.getPaddingEnd(view); + } + + /** + * Sets the relative padding. The view may add on the space required to display + * the scrollbars, depending on the style and visibility of the scrollbars. + * So the values returned from {@link #getPaddingStart}, {@link View#getPaddingTop}, + * {@link #getPaddingEnd} and {@link View#getPaddingBottom} may be different + * from the values set in this call. + * + * @param view The view on which to set relative padding + * @param start the start padding in pixels + * @param top the top padding in pixels + * @param end the end padding in pixels + * @param bottom the bottom padding in pixels + */ + public static void setPaddingRelative(View view, int start, int top, int end, int bottom) { + IMPL.setPaddingRelative(view, start, top, end, bottom); + } + + /** + * Notify a view that it is being temporarily detached. + */ + public static void dispatchStartTemporaryDetach(View view) { + IMPL.dispatchStartTemporaryDetach(view); + } + + /** + * Notify a view that its temporary detach has ended; the view is now reattached. + */ + public static void dispatchFinishTemporaryDetach(View view) { + IMPL.dispatchFinishTemporaryDetach(view); + } + + /** + * The horizontal location of this view relative to its {@link View#getLeft() left} position. + * This position is post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The horizontal position of this view relative to its left position, in pixels. + */ + public static float getTranslationX(View view) { + return IMPL.getTranslationX(view); + } + + /** + * The vertical location of this view relative to its {@link View#getTop() left} position. + * This position is post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The vertical position of this view relative to its top position, in pixels. + */ + public static float getTranslationY(View view) { + return IMPL.getTranslationY(view); + } + + /** + * Returns the minimum width of the view. + * + *

    Prior to API 16 this will return 0.

    + * + * @return the minimum width the view will try to be. + */ + public static int getMinimumWidth(View view) { + return IMPL.getMinimumWidth(view); + } + + /** + * Returns the minimum height of the view. + * + *

    Prior to API 16 this will return 0.

    + * + * @return the minimum height the view will try to be. + */ + public static int getMinimumHeight(View view) { + return IMPL.getMinimumHeight(view); + } + + /** + * This method returns a ViewPropertyAnimator object, which can be used to animate + * specific properties on this View. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @return ViewPropertyAnimator The ViewPropertyAnimator associated with this View. + */ + public static ViewPropertyAnimatorCompat animate(View view) { + return IMPL.animate(view); + } + + /** + * Sets the horizontal location of this view relative to its left position. + * This effectively positions the object post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The horizontal position of this view relative to its left position, + * in pixels. + */ + public static void setTranslationX(View view, float value) { + IMPL.setTranslationX(view, value); + } + + /** + * Sets the vertical location of this view relative to its top position. + * This effectively positions the object post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The vertical position of this view relative to its top position, + * in pixels. + * + * @attr ref android.R.styleable#View_translationY + */ + public static void setTranslationY(View view, float value) { + IMPL.setTranslationY(view, value); + } + + /** + *

    Sets the opacity of the view. This is a value from 0 to 1, where 0 means the view is + * completely transparent and 1 means the view is completely opaque.

    + * + *

    Note that setting alpha to a translucent value (0 < alpha < 1) can have significant + * performance implications, especially for large views. It is best to use the alpha property + * sparingly and transiently, as in the case of fading animations.

    + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The opacity of the view. + */ + public static void setAlpha(View view, float value) { + IMPL.setAlpha(view, value); + } + + /** + * Sets the visual x position of this view, in pixels. This is equivalent to setting the + * {@link #setTranslationX(View, float) translationX} property to be the difference between + * the x value passed in and the current left property of the view as determined + * by the layout bounds. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The visual x position of this view, in pixels. + */ + public static void setX(View view, float value) { + IMPL.setX(view, value); + } + + /** + * Sets the visual y position of this view, in pixels. This is equivalent to setting the + * {@link #setTranslationY(View, float) translationY} property to be the difference between + * the y value passed in and the current top property of the view as determined by the + * layout bounds. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The visual y position of this view, in pixels. + */ + public static void setY(View view, float value) { + IMPL.setY(view, value); + } + + /** + * Sets the degrees that the view is rotated around the pivot point. Increasing values + * result in clockwise rotation. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of rotation. + */ + public static void setRotation(View view, float value) { + IMPL.setRotation(view, value); + } + + /** + * Sets the degrees that the view is rotated around the horizontal axis through the pivot point. + * Increasing values result in clockwise rotation from the viewpoint of looking down the + * x axis. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of X rotation. + */ + public static void setRotationX(View view, float value) { + IMPL.setRotationX(view, value); + } + + /** + * Sets the degrees that the view is rotated around the vertical axis through the pivot point. + * Increasing values result in counter-clockwise rotation from the viewpoint of looking + * down the y axis. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of Y rotation. + */ + public static void setRotationY(View view, float value) { + IMPL.setRotationY(view, value); + } + + /** + * Sets the amount that the view is scaled in x around the pivot point, as a proportion of + * the view's unscaled width. A value of 1 means that no scaling is applied. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The scaling factor. + */ + public static void setScaleX(View view, float value) { + IMPL.setScaleX(view, value); + } + + /** + * Sets the amount that the view is scaled in Y around the pivot point, as a proportion of + * the view's unscaled width. A value of 1 means that no scaling is applied. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The scaling factor. + */ + public static void setScaleY(View view, float value) { + IMPL.setScaleY(view, value); + } + + /** + * The x location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleX(View, float) scaled}. + * + *

    Prior to API 11 this will have no effect.

    + * + */ + public static float getPivotX(View view) { + return IMPL.getPivotX(view); + } + + /** + * Sets the x location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleX(View, float) scaled}. + * By default, the pivot point is centered on the object. + * Setting this property disables this behavior and causes the view to use only the + * explicitly set pivotX and pivotY values. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The x location of the pivot point. + */ + public static void setPivotX(View view, float value) { + IMPL.setPivotX(view, value); + } + + /** + * The y location of the point around which the view is {@link #setRotation(View, + * float) rotated} and {@link #setScaleY(View, float) scaled}. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The y location of the pivot point. + */ + public static float getPivotY(View view) { + return IMPL.getPivotY(view); + } + + /** + * Sets the y location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleY(View, float) scaled}. + * By default, the pivot point is centered on the object. + * Setting this property disables this behavior and causes the view to use only the + * explicitly set pivotX and pivotY values. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The y location of the pivot point. + */ + public static void setPivotY(View view, float value) { + IMPL.setPivotX(view, value); + } + + public static float getRotation(View view) { + return IMPL.getRotation(view); + } + + public static float getRotationX(View view) { + return IMPL.getRotationX(view); + } + + public static float getRotationY(View view) { + return IMPL.getRotationY(view); + } + + public static float getScaleX(View view) { + return IMPL.getScaleX(view); + } + + public static float getScaleY(View view) { + return IMPL.getScaleY(view); + } + + public static float getX(View view) { + return IMPL.getX(view); + } + + public static float getY(View view) { + return IMPL.getY(view); + } + + /** + * Sets the base elevation of this view, in pixels. + */ + public static void setElevation(View view, float elevation) { + IMPL.setElevation(view, elevation); + } + + /** + * The base elevation of this view relative to its parent, in pixels. + * + * @return The base depth position of the view, in pixels. + */ + public static float getElevation(View view) { + return IMPL.getElevation(view); + } + + /** + * Sets the depth location of this view relative to its {@link #getElevation(View) elevation}. + */ + public static void setTranslationZ(View view, float translationZ) { + IMPL.setTranslationZ(view, translationZ); + } + + /** + * The depth location of this view relative to its {@link #getElevation(View) elevation}. + * + * @return The depth of this view relative to its elevation. + */ + public static float getTranslationZ(View view) { + return IMPL.getTranslationZ(view); + } + + /** + * Sets the name of the View to be used to identify Views in Transitions. + * Names should be unique in the View hierarchy. + * + * @param view The View against which to invoke the method. + * @param transitionName The name of the View to uniquely identify it for Transitions. + */ + public static void setTransitionName(View view, String transitionName) { + IMPL.setTransitionName(view, transitionName); + } + + /** + * Returns the name of the View to be used to identify Views in Transitions. + * Names should be unique in the View hierarchy. + * + *

    This returns null if the View has not been given a name.

    + * + * @param view The View against which to invoke the method. + * @return The name used of the View to be used to identify Views in Transitions or null + * if no name has been given. + */ + public static String getTransitionName(View view) { + return IMPL.getTransitionName(view); + } + + /** + * Returns the current system UI visibility that is currently set for the entire window. + */ + public static int getWindowSystemUiVisibility(View view) { + return IMPL.getWindowSystemUiVisibility(view); + } + + /** + * Ask that a new dispatch of {@code View.onApplyWindowInsets(WindowInsets)} be performed. This + * falls back to {@code View.requestFitSystemWindows()} where available. + */ + public static void requestApplyInsets(View view) { + IMPL.requestApplyInsets(view); + } + + /** + * Tells the ViewGroup whether to draw its children in the order defined by the method + * {@code ViewGroup.getChildDrawingOrder(int, int)}. + * + * @param enabled true if the order of the children when drawing is determined by + * {@link ViewGroup#getChildDrawingOrder(int, int)}, false otherwise + * + *

    Prior to API 7 this will have no effect.

    + */ + public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + IMPL.setChildrenDrawingOrderEnabled(viewGroup, enabled); + } + + /** + * Returns true if this view should adapt to fit system window insets. This method will always + * return false before API 16 (Jellybean). + */ + public static boolean getFitsSystemWindows(View v) { + return IMPL.getFitsSystemWindows(v); + } + + /** + * Sets whether or not this view should account for system screen decorations + * such as the status bar and inset its content; that is, controlling whether + * the default implementation of {@link View#fitSystemWindows(Rect)} will be + * executed. See that method for more details. + */ + public static void setFitsSystemWindows(View view, boolean fitSystemWindows) { + IMPL.setFitsSystemWindows(view, fitSystemWindows); + } + + /** + * On API 11 devices and above, call Drawable.jumpToCurrentState() + * on all Drawable objects associated with this view. + *

    + * On API 21 and above, also calls StateListAnimator#jumpToCurrentState() + * if there is a StateListAnimator attached to this view. + */ + public static void jumpDrawablesToCurrentState(View v) { + IMPL.jumpDrawablesToCurrentState(v); + } + + /** + * Set an {@link OnApplyWindowInsetsListener} to take over the policy for applying + * window insets to this view. This will only take effect on devices with API 21 or above. + */ + public static void setOnApplyWindowInsetsListener(View v, + OnApplyWindowInsetsListener listener) { + IMPL.setOnApplyWindowInsetsListener(v, listener); + } + + /** + * Called when the view should apply {@link WindowInsetsCompat} according to its internal policy. + * + *

    Clients may supply an {@link OnApplyWindowInsetsListener} to a view. If one is set + * it will be called during dispatch instead of this method. The listener may optionally + * call this method from its own implementation if it wishes to apply the view's default + * insets policy in addition to its own.

    + * + * @param view The View against which to invoke the method. + * @param insets Insets to apply + * @return The supplied insets with any applied insets consumed + */ + public static WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) { + return IMPL.onApplyWindowInsets(view, insets); + } + + /** + * Request to apply the given window insets to this view or another view in its subtree. + * + *

    This method should be called by clients wishing to apply insets corresponding to areas + * obscured by window decorations or overlays. This can include the status and navigation bars, + * action bars, input methods and more. New inset categories may be added in the future. + * The method returns the insets provided minus any that were applied by this view or its + * children.

    + * + * @param insets Insets to apply + * @return The provided insets minus the insets that were consumed + */ + public static WindowInsetsCompat dispatchApplyWindowInsets(View view, + WindowInsetsCompat insets) { + return IMPL.dispatchApplyWindowInsets(view, insets); + } + + /** + * Controls whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. + * + * @param enabled Set to false to disable state saving, or true + * (the default) to allow it. + */ + public static void setSaveFromParentEnabled(View v, boolean enabled) { + IMPL.setSaveFromParentEnabled(v, enabled); + } + + /** + * Changes the activated state of this view. A view can be activated or not. + * Note that activation is not the same as selection. Selection is + * a transient property, representing the view (hierarchy) the user is + * currently interacting with. Activation is a longer-term state that the + * user can move views in and out of. + * + * @param activated true if the view must be activated, false otherwise + */ + public static void setActivated(View view, boolean activated) { + IMPL.setActivated(view, activated); + } + + /** + * Return if the padding as been set through relative values + * {@code View.setPaddingRelative(int, int, int, int)} or thru + * + * @return true if the padding is relative or false if it is not. + */ + public static boolean isPaddingRelative(View view) { + return IMPL.isPaddingRelative(view); + } + + /** + * Return the tint applied to the background drawable, if specified. + *

    + * Only returns meaningful info when running on API v21 or newer, or if {@code view} + * implements the {@code TintableBackgroundView} interface. + */ + public static ColorStateList getBackgroundTintList(View view) { + return IMPL.getBackgroundTintList(view); + } + + /** + * Applies a tint to the background drawable. + *

    + * This will always take effect when running on API v21 or newer. When running on platforms + * previous to API v21, it will only take effect if {@code view} implement the + * {@code TintableBackgroundView} interface. + */ + public static void setBackgroundTintList(View view, ColorStateList tintList) { + IMPL.setBackgroundTintList(view, tintList); + } + + /** + * Return the blending mode used to apply the tint to the background + * drawable, if specified. + *

    + * Only returns meaningful info when running on API v21 or newer, or if {@code view} + * implements the {@code TintableBackgroundView} interface. + */ + public static PorterDuff.Mode getBackgroundTintMode(View view) { + return IMPL.getBackgroundTintMode(view); + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setBackgroundTintList(android.view.View, android.content.res.ColorStateList)} to + * the background drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. + *

    + * This will always take effect when running on API v21 or newer. When running on platforms + * previous to API v21, it will only take effect if {@code view} implement the + * {@code TintableBackgroundView} interface. + */ + public static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + IMPL.setBackgroundTintMode(view, mode); + } + // TODO: getters for various view properties (rotation, etc) + + /** + * Enable or disable nested scrolling for this view. + * + *

    If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of + * {@link #stopNestedScroll(View) stopping} the nested scroll.

    + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled(View) + */ + public static void setNestedScrollingEnabled(View view, boolean enabled) { + IMPL.setNestedScrollingEnabled(view, enabled); + } + + /** + * Returns true if nested scrolling is enabled for this view. + * + *

    If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.

    + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(View, boolean) + */ + public static boolean isNestedScrollingEnabled(View view) { + return IMPL.isNestedScrollingEnabled(view); + } + + /** + * Begin a nestable scroll operation along the given axes. + * + *

    A view starting a nested scroll promises to abide by the following contract:

    + * + *

    The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll(View)} to indicate the end of the nested scroll.

    + * + *

    If startNestedScroll returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.

    + * + *

    At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(View, int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.

    + * + *

    After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(View, int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + *

    + * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll(View) + * @see #dispatchNestedPreScroll(View, int, int, int[], int[]) + * @see #dispatchNestedScroll(View, int, int, int, int, int[]) + */ + public static boolean startNestedScroll(View view, int axes) { + return IMPL.startNestedScroll(view, axes); + } + + /** + * Stop a nested scroll in progress. + * + *

    Calling this method when a nested scroll is not currently in progress is harmless.

    + * + * @see #startNestedScroll(View, int) + */ + public static void stopNestedScroll(View view) { + IMPL.stopNestedScroll(view); + } + + /** + * Returns true if this view has a nested scrolling parent. + * + *

    The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.

    + * + * @return whether this view has a nested scrolling parent + */ + public static boolean hasNestedScrollingParent(View view) { + return IMPL.hasNestedScrollingParent(view); + } + + /** + * Dispatch one step of a nested scroll in progress. + * + *

    Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled(View) enabled} for this view this method does nothing.

    + * + *

    Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(View, int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.

    + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(View, int, int, int[], int[]) + */ + public static boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return IMPL.dispatchNestedScroll(view, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + *

    Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * dispatchNestedPreScroll offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.

    + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(View, int, int, int, int, int[]) + */ + public static boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow) { + return IMPL.dispatchNestedPreScroll(view, dx, dy, consumed, offsetInWindow); + } + + /** + * Dispatch a fling to a nested scrolling parent. + * + *

    This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + public static boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return IMPL.dispatchNestedFling(view, velocityX, velocityY, consumed); + } + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + *

    Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. dispatchNestedPreFling + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns true, a nested + * parent view consumed the fling and this view should not scroll as a result.

    + * + *

    For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:

    + * + *
      + *
    • If a custom view is paged and needs to settle to a fixed page-point, do not + * call dispatchNestedPreFling; consume the fling and settle to a valid + * position regardless.
    • + *
    • If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.
    • + *
    + * + *

    Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + public static boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return IMPL.dispatchNestedPreFling(view, velocityX, velocityY); + } + + /** + * Returns true if {@code view} has been through at least one layout since it + * was last attached to or detached from a window. + */ + public static boolean isLaidOut(View view) { + return IMPL.isLaidOut(view); + } + + /** + * The visual z position of this view, in pixels. This is equivalent to the + * {@link #setTranslationZ(View, float) translationZ} property plus the current + * {@link #getElevation(View) elevation} property. + * + * @return The visual z position of this view, in pixels. + */ + public static float getZ(View view) { + return IMPL.getZ(view); + } + + /** + * Offset this view's vertical location by the specified number of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public static void offsetTopAndBottom(View view, int offset) { + view.offsetTopAndBottom(offset); + + if (offset != 0 && Build.VERSION.SDK_INT < 11) { + // We need to manually invalidate pre-honeycomb + view.invalidate(); + } + } + /** + * Offset this view's horizontal location by the specified amount of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public static void offsetLeftAndRight(View view, int offset) { + view.offsetLeftAndRight(offset); + + if (offset != 0 && Build.VERSION.SDK_INT < 11) { + // We need to manually invalidate pre-honeycomb + view.invalidate(); + } + } + + /** + * Returns true if the provided view is currently attached to a window. + */ + public static boolean isAttachedToWindow(View view) { + return IMPL.isAttachedToWindow(view); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java new file mode 100644 index 00000000000..33706592496 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java @@ -0,0 +1,111 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.view.View; + +import java.lang.reflect.Field; + +class ViewCompatBase { + + private static final String TAG = "ViewCompatBase"; + + private static Field sMinWidthField; + private static boolean sMinWidthFieldFetched; + private static Field sMinHeightField; + private static boolean sMinHeightFieldFetched; + + static ColorStateList getBackgroundTintList(View view) { + return (view instanceof TintableBackgroundView) + ? ((TintableBackgroundView) view).getSupportBackgroundTintList() + : null; + } + + static void setBackgroundTintList(View view, ColorStateList tintList) { + if (view instanceof TintableBackgroundView) { + ((TintableBackgroundView) view).setSupportBackgroundTintList(tintList); + } + } + + static PorterDuff.Mode getBackgroundTintMode(View view) { + return (view instanceof TintableBackgroundView) + ? ((TintableBackgroundView) view).getSupportBackgroundTintMode() + : null; + } + + static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + if (view instanceof TintableBackgroundView) { + ((TintableBackgroundView) view).setSupportBackgroundTintMode(mode); + } + } + + static boolean isLaidOut(View view) { + return view.getWidth() > 0 && view.getHeight() > 0; + } + + static int getMinimumWidth(View view) { + if (!sMinWidthFieldFetched) { + try { + sMinWidthField = View.class.getDeclaredField("mMinWidth"); + sMinWidthField.setAccessible(true); + } catch (NoSuchFieldException e) { + // Couldn't find the field. Abort! + } + sMinWidthFieldFetched = true; + } + + if (sMinWidthField != null) { + try { + return (int) sMinWidthField.get(view); + } catch (Exception e) { + // Field get failed. Oh well... + } + } + + // We failed, return 0 + return 0; + } + + static int getMinimumHeight(View view) { + if (!sMinHeightFieldFetched) { + try { + sMinHeightField = View.class.getDeclaredField("mMinHeight"); + sMinHeightField.setAccessible(true); + } catch (NoSuchFieldException e) { + // Couldn't find the field. Abort! + } + sMinHeightFieldFetched = true; + } + + if (sMinHeightField != null) { + try { + return (int) sMinHeightField.get(view); + } catch (Exception e) { + // Field get failed. Oh well... + } + } + + // We failed, return 0 + return 0; + } + + static boolean isAttachedToWindow(View view) { + return view.getWindowToken() != null; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java new file mode 100644 index 00000000000..971b70d5f1f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +class ViewCompatEclairMr1 { + public static final String TAG = "ViewCompat"; + + private static Method sChildrenDrawingOrderMethod; + + public static boolean isOpaque(View view) { + return view.isOpaque(); + } + + public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + if (sChildrenDrawingOrderMethod == null) { + try { + sChildrenDrawingOrderMethod = ViewGroup.class + .getDeclaredMethod("setChildrenDrawingOrderEnabled", boolean.class); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Unable to find childrenDrawingOrderEnabled", e); + } + sChildrenDrawingOrderMethod.setAccessible(true); + } + try { + sChildrenDrawingOrderMethod.invoke(viewGroup, enabled); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java new file mode 100644 index 00000000000..f410a6941fd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.View; + +class ViewCompatGingerbread { + public static int getOverScrollMode(View v) { + return v.getOverScrollMode(); + } + + public static void setOverScrollMode(View v, int mode) { + v.setOverScrollMode(mode); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java new file mode 100644 index 00000000000..8b212597b71 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java @@ -0,0 +1,163 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.animation.ValueAnimator; +import android.graphics.Paint; +import android.view.View; + +class ViewCompatHC { + static long getFrameTime() { + return ValueAnimator.getFrameDelay(); + } + + public static float getAlpha(View view) { + return view.getAlpha(); + } + + public static void setLayerType(View view, int layerType, Paint paint) { + view.setLayerType(layerType, paint); + } + + public static int getLayerType(View view) { + return view.getLayerType(); + } + + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return View.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + + public static int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidthAndState(); + } + + public static int getMeasuredHeightAndState(View view) { + return view.getMeasuredHeightAndState(); + } + + public static int getMeasuredState(View view) { + return view.getMeasuredState(); + } + + public static float getTranslationX(View view) { + return view.getTranslationX(); + } + + public static float getTranslationY(View view) { + return view.getTranslationY(); + } + + public static float getX(View view) { + return view.getX(); + } + + public static float getY(View view) { + return view.getY(); + } + + public static float getRotation(View view) { + return view.getRotation(); + } + + public static float getRotationX(View view) { + return view.getRotationX(); + } + + public static float getRotationY(View view) { + return view.getRotationY(); + } + + public static float getScaleX(View view) { + return view.getScaleX(); + } + + public static float getScaleY(View view) { + return view.getScaleY(); + } + + public static void setTranslationX(View view, float value) { + view.setTranslationX(value); + } + + public static void setTranslationY(View view, float value) { + view.setTranslationY(value); + } + + public static void setAlpha(View view, float value) { + view.setAlpha(value); + } + + public static void setX(View view, float value) { + view.setX(value); + } + + public static void setY(View view, float value) { + view.setY(value); + } + + public static void setRotation(View view, float value) { + view.setRotation(value); + } + + public static void setRotationX(View view, float value) { + view.setRotationX(value); + } + + public static void setRotationY(View view, float value) { + view.setRotationY(value); + } + + public static void setScaleX(View view, float value) { + view.setScaleX(value); + } + + public static void setScaleY(View view, float value) { + view.setScaleY(value); + } + + public static void setPivotX(View view, float value) { + view.setPivotX(value); + } + + public static void setPivotY(View view, float value) { + view.setPivotY(value); + } + + public static float getPivotX(View view) { + return view.getPivotX(); + } + + public static float getPivotY(View view) { + return view.getPivotY(); + } + + public static void jumpDrawablesToCurrentState(View view) { + view.jumpDrawablesToCurrentState(); + } + + public static void setSaveFromParentEnabled(View view, boolean enabled) { + view.setSaveFromParentEnabled(enabled); + } + + public static void setActivated(View view, boolean activated) { + view.setActivated(activated); + } + + public static int combineMeasuredStates(int curState, int newState) { + return View.combineMeasuredStates(curState, newState); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java new file mode 100644 index 00000000000..742c47c5867 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.support.annotation.Nullable; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * Helper for accessing newer features in View introduced in ICS. + */ +class ViewCompatICS { + + public static boolean canScrollHorizontally(View v, int direction) { + return v.canScrollHorizontally(direction); + } + + public static boolean canScrollVertically(View v, int direction) { + return v.canScrollVertically(direction); + } + + public static void setAccessibilityDelegate(View v, @Nullable Object delegate) { + v.setAccessibilityDelegate((AccessibilityDelegate) delegate); + } + + public static void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + v.onPopulateAccessibilityEvent(event); + } + + public static void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + v.onInitializeAccessibilityEvent(event); + } + + public static void onInitializeAccessibilityNodeInfo(View v, Object info) { + v.onInitializeAccessibilityNodeInfo((AccessibilityNodeInfo) info); + } + + public static void setFitsSystemWindows(View view, boolean fitSystemWindows) { + view.setFitsSystemWindows(fitSystemWindows); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java new file mode 100644 index 00000000000..93735724a38 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java @@ -0,0 +1,88 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.os.Bundle; +import android.view.View; +import android.view.ViewParent; + +/** + * Jellybean-specific View API access + */ +class ViewCompatJB { + + public static boolean hasTransientState(View view) { + return view.hasTransientState(); + } + + public static void setHasTransientState(View view, boolean hasTransientState) { + view.setHasTransientState(hasTransientState); + } + + public static void postInvalidateOnAnimation(View view) { + view.postInvalidateOnAnimation(); + } + + public static void postInvalidateOnAnimation(View view, int left, int top, + int right, int bottom) { + view.postInvalidate(left, top, right, bottom); + } + + public static void postOnAnimation(View view, Runnable action) { + view.postOnAnimation(action); + } + + public static void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + view.postOnAnimationDelayed(action, delayMillis); + } + + public static int getImportantForAccessibility(View view) { + return view.getImportantForAccessibility(); + } + + public static void setImportantForAccessibility(View view, int mode) { + view.setImportantForAccessibility(mode); + } + + public static boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return view.performAccessibilityAction(action, arguments); + } + + public static Object getAccessibilityNodeProvider(View view) { + return view.getAccessibilityNodeProvider(); + } + + public static ViewParent getParentForAccessibility(View view) { + return view.getParentForAccessibility(); + } + + public static int getMinimumWidth(View view) { + return view.getMinimumWidth(); + } + + public static int getMinimumHeight(View view) { + return view.getMinimumHeight(); + } + + public static void requestApplyInsets(View view) { + view.requestFitSystemWindows(); + } + + public static boolean getFitsSystemWindows(View view) { + return view.getFitsSystemWindows(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java new file mode 100644 index 00000000000..4865a8ba4de --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.graphics.Paint; +import android.view.View; + +/** + * Jellybean MR1 - specific View API access. + */ +class ViewCompatJellybeanMr1 { + + public static int getLabelFor(View view) { + return view.getLabelFor(); + } + + public static void setLabelFor(View view, int id) { + view.setLabelFor(id); + } + + public static void setLayerPaint(View view, Paint paint) { + view.setLayerPaint(paint); + } + + public static int getLayoutDirection(View view) { + return view.getLayoutDirection(); + } + + public static void setLayoutDirection(View view, int layoutDirection) { + view.setLayoutDirection(layoutDirection); + } + + public static int getPaddingStart(View view) { + return view.getPaddingStart(); + } + + public static int getPaddingEnd(View view) { + return view.getPaddingEnd(); + } + + public static void setPaddingRelative(View view, int start, int top, int end, int bottom) { + view.setPaddingRelative(start, top, end, bottom); + } + + public static int getWindowSystemUiVisibility(View view) { + return view.getWindowSystemUiVisibility(); + } + + public static boolean isPaddingRelative(View view) { + return view.isPaddingRelative(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java new file mode 100644 index 00000000000..cf126d9c625 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * KitKat-specific View API implementation. + */ +class ViewCompatKitKat { + public static int getAccessibilityLiveRegion(View view) { + return view.getAccessibilityLiveRegion(); + } + + public static void setAccessibilityLiveRegion(View view, int mode) { + view.setAccessibilityLiveRegion(mode); + } + + public static boolean isLaidOut(View view) { + return view.isLaidOut(); + } + + public static boolean isAttachedToWindow(View view) { + return view.isAttachedToWindow(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java new file mode 100644 index 00000000000..09b84b3c4bb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java @@ -0,0 +1,162 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.view.View; +import android.view.WindowInsets; + +class ViewCompatLollipop { + + public static void setTransitionName(View view, String transitionName) { + view.setTransitionName(transitionName); + } + + public static String getTransitionName(View view) { + return view.getTransitionName(); + } + + public static void requestApplyInsets(View view) { + view.requestApplyInsets(); + } + + public static void setElevation(View view, float elevation) { + view.setElevation(elevation); + } + + public static float getElevation(View view) { + return view.getElevation(); + } + + public static void setTranslationZ(View view, float translationZ) { + view.setTranslationZ(translationZ); + } + + public static float getTranslationZ(View view) { + return view.getTranslationZ(); + } + + public static void setOnApplyWindowInsetsListener(View view, + final OnApplyWindowInsetsListener listener) { + view.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { + // Wrap the framework insets in our wrapper + WindowInsetsCompatApi21 insets = new WindowInsetsCompatApi21(windowInsets); + // Give the listener a chance to use the wrapped insets + insets = (WindowInsetsCompatApi21) listener.onApplyWindowInsets(view, insets); + // Return the unwrapped insets + return insets.unwrap(); + } + }); + } + + public static boolean isImportantForAccessibility(View view) { + return view.isImportantForAccessibility(); + } + + static ColorStateList getBackgroundTintList(View view) { + return view.getBackgroundTintList(); + } + + static void setBackgroundTintList(View view, ColorStateList tintList) { + view.setBackgroundTintList(tintList); + } + + static PorterDuff.Mode getBackgroundTintMode(View view) { + return view.getBackgroundTintMode(); + } + + static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + view.setBackgroundTintMode(mode); + } + + public static WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + if (insets instanceof WindowInsetsCompatApi21) { + // First unwrap the compat version so that we have the framework instance + WindowInsets unwrapped = ((WindowInsetsCompatApi21) insets).unwrap(); + // Now call onApplyWindowInsets + WindowInsets result = v.onApplyWindowInsets(unwrapped); + + if (result != unwrapped) { + // ...and return a newly wrapped compat insets instance if different + insets = new WindowInsetsCompatApi21(result); + } + } + return insets; + } + + public static WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + if (insets instanceof WindowInsetsCompatApi21) { + // First unwrap the compat version so that we have the framework instance + WindowInsets unwrapped = ((WindowInsetsCompatApi21) insets).unwrap(); + // Now call dispatchApplyWindowInsets + WindowInsets result = v.dispatchApplyWindowInsets(unwrapped); + + if (result != unwrapped) { + // ...and return a newly wrapped compat insets instance if different + insets = new WindowInsetsCompatApi21(result); + } + } + return insets; + } + + public static void setNestedScrollingEnabled(View view, boolean enabled) { + view.setNestedScrollingEnabled(enabled); + } + + public static boolean isNestedScrollingEnabled(View view) { + return view.isNestedScrollingEnabled(); + } + + public static boolean startNestedScroll(View view, int axes) { + return view.startNestedScroll(axes); + } + + public static void stopNestedScroll(View view) { + view.stopNestedScroll(); + } + + public static boolean hasNestedScrollingParent(View view) { + return view.hasNestedScrollingParent(); + } + + public static boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return view.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + public static boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow) { + return view.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + public static boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return view.dispatchNestedFling(velocityX, velocityY, consumed); + } + + public static boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return view.dispatchNestedPreFling(velocityX, velocityY); + } + + public static float getZ(View view) { + return view.getZ(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java new file mode 100644 index 00000000000..86d19bbc736 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.ViewConfiguration; + +/** + * Helper for accessing features in {@link ViewConfiguration} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewConfigurationCompat { + /** + * Interface for the full API. + */ + interface ViewConfigurationVersionImpl { + public int getScaledPagingTouchSlop(ViewConfiguration config); + public boolean hasPermanentMenuKey(ViewConfiguration config); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseViewConfigurationVersionImpl implements ViewConfigurationVersionImpl { + @Override + public int getScaledPagingTouchSlop(ViewConfiguration config) { + return config.getScaledTouchSlop(); + } + + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + // Pre-HC devices will always have a menu button + return true; + } + } + + /** + * Interface implementation for devices with at least v8 APIs. + */ + static class FroyoViewConfigurationVersionImpl extends BaseViewConfigurationVersionImpl { + @Override + public int getScaledPagingTouchSlop(ViewConfiguration config) { + return ViewConfigurationCompatFroyo.getScaledPagingTouchSlop(config); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombViewConfigurationVersionImpl extends FroyoViewConfigurationVersionImpl { + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + // There is no way to check on Honeycomb so we assume false + return false; + } + } + + /** + * Interface implementation for devices with at least v14 APIs. + */ + static class IcsViewConfigurationVersionImpl extends HoneycombViewConfigurationVersionImpl { + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + return ViewConfigurationCompatICS.hasPermanentMenuKey(config); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final ViewConfigurationVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new IcsViewConfigurationVersionImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombViewConfigurationVersionImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 8) { + IMPL = new FroyoViewConfigurationVersionImpl(); + } else { + IMPL = new BaseViewConfigurationVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Call {@link ViewConfiguration#getScaledPagingTouchSlop()}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#FROYO} device, + * returns {@link ViewConfiguration#getScaledTouchSlop()}. + */ + public static int getScaledPagingTouchSlop(ViewConfiguration config) { + return IMPL.getScaledPagingTouchSlop(config); + } + + /** + * Report if the device has a permanent menu key available to the user, in a backwards + * compatible way. + */ + public static boolean hasPermanentMenuKey(ViewConfiguration config) { + return IMPL.hasPermanentMenuKey(config); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java new file mode 100644 index 00000000000..71a4b26328e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.ViewConfiguration; + +/** + * Implementation of menu compatibility that can call Froyo APIs. + */ +class ViewConfigurationCompatFroyo { + public static int getScaledPagingTouchSlop(ViewConfiguration config) { + return config.getScaledPagingTouchSlop(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java new file mode 100644 index 00000000000..13a96471abc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.ViewConfiguration; + +/** + * Implementation of menu compatibility that can call ICS APIs. + */ +class ViewConfigurationCompatICS { + static boolean hasPermanentMenuKey(ViewConfiguration config) { + return config.hasPermanentMenuKey(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java new file mode 100644 index 00000000000..2254c4aca97 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing features in {@link ViewGroup} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewGroupCompat { + + /** + * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. + * Clip bounds are the raw values of {@link android.view.View#getLeft() left}, + * {@link android.view.View#getTop() top}, + * {@link android.view.View#getRight() right} and {@link android.view.View#getBottom() bottom}. + */ + public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; + + /** + * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. + * Optical bounds describe where a widget appears to be. They sit inside the clip + * bounds which need to cover a larger area to allow other effects, + * such as shadows and glows, to be drawn. + */ + public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1; + + interface ViewGroupCompatImpl { + boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event); + void setMotionEventSplittingEnabled(ViewGroup group, boolean split); + int getLayoutMode(ViewGroup group); + void setLayoutMode(ViewGroup group, int mode); + void setTransitionGroup(ViewGroup group, boolean isTransitionGroup); + boolean isTransitionGroup(ViewGroup group); + int getNestedScrollAxes(ViewGroup group); + } + + static class ViewGroupCompatStubImpl implements ViewGroupCompatImpl { + public boolean onRequestSendAccessibilityEvent( + ViewGroup group, View child, AccessibilityEvent event) { + return true; + } + + public void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + // no-op, didn't exist. + } + + @Override + public int getLayoutMode(ViewGroup group) { + return LAYOUT_MODE_CLIP_BOUNDS; + } + + @Override + public void setLayoutMode(ViewGroup group, int mode) { + // no-op, didn't exist. Views only support clip bounds. + } + + @Override + public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + } + + @Override + public boolean isTransitionGroup(ViewGroup group) { + return false; + } + + @Override + public int getNestedScrollAxes(ViewGroup group) { + if (group instanceof NestedScrollingParent) { + return ((NestedScrollingParent) group).getNestedScrollAxes(); + } + return ViewCompat.SCROLL_AXIS_NONE; + } + } + + static class ViewGroupCompatHCImpl extends ViewGroupCompatStubImpl { + @Override + public void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + ViewGroupCompatHC.setMotionEventSplittingEnabled(group, split); + } + } + + static class ViewGroupCompatIcsImpl extends ViewGroupCompatHCImpl { + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup group, View child, AccessibilityEvent event) { + return ViewGroupCompatIcs.onRequestSendAccessibilityEvent(group, child, event); + } + } + + static class ViewGroupCompatJellybeanMR2Impl extends ViewGroupCompatIcsImpl { + @Override + public int getLayoutMode(ViewGroup group) { + return ViewGroupCompatJellybeanMR2.getLayoutMode(group); + } + + @Override + public void setLayoutMode(ViewGroup group, int mode) { + ViewGroupCompatJellybeanMR2.setLayoutMode(group, mode); + } + } + + static class ViewGroupCompatLollipopImpl extends ViewGroupCompatJellybeanMR2Impl { + @Override + public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + ViewGroupCompatLollipop.setTransitionGroup(group, isTransitionGroup); + } + + @Override + public boolean isTransitionGroup(ViewGroup group) { + return ViewGroupCompatLollipop.isTransitionGroup(group); + } + + @Override + public int getNestedScrollAxes(ViewGroup group) { + return ViewGroupCompatLollipop.getNestedScrollAxes(group); + } + } + + static final ViewGroupCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new ViewGroupCompatLollipopImpl(); + } else if (version >= 18) { + IMPL = new ViewGroupCompatJellybeanMR2Impl(); + } else if (version >= 14) { + IMPL = new ViewGroupCompatIcsImpl(); + } else if (version >= 11) { + IMPL = new ViewGroupCompatHCImpl(); + } else { + IMPL = new ViewGroupCompatStubImpl(); + } + } + + /* + * Hide the constructor. + */ + private ViewGroupCompat() { + + } + + /** + * Called when a child has requested sending an {@link AccessibilityEvent} and + * gives an opportunity to its parent to augment the event. + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link ViewCompat#setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#onRequestSendAccessibilityEvent(ViewGroup, View, + * AccessibilityEvent)} is responsible for handling this call. + *

    + * + * @param group The group whose method to invoke. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent. + */ + public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event) { + return IMPL.onRequestSendAccessibilityEvent(group, child, event); + } + + /** + * Enable or disable the splitting of MotionEvents to multiple children during touch event + * dispatch. This behavior is enabled by default for applications that target an + * SDK version of 11 (Honeycomb) or newer. On earlier platform versions this feature + * was not supported and this method is a no-op. + * + *

    When this option is enabled MotionEvents may be split and dispatched to different child + * views depending on where each pointer initially went down. This allows for user interactions + * such as scrolling two panes of content independently, chording of buttons, and performing + * independent gestures on different pieces of content. + * + * @param group ViewGroup to modify + * @param split true to allow MotionEvents to be split and dispatched to multiple + * child views. false to only allow one child view to be the target of + * any MotionEvent received by this ViewGroup. + */ + public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + IMPL.setMotionEventSplittingEnabled(group, split); + } + + /** + * Returns the basis of alignment during layout operations on this ViewGroup: + * either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. + *

    + * If no layoutMode was explicitly set, either programmatically or in an XML resource, + * the method returns the layoutMode of the view's parent ViewGroup if such a parent exists, + * otherwise the method returns a default value of {@link #LAYOUT_MODE_CLIP_BOUNDS}. + * + * @return the layout mode to use during layout operations + * + * @see #setLayoutMode(ViewGroup, int) + */ + public static int getLayoutMode(ViewGroup group) { + return IMPL.getLayoutMode(group); + } + + /** + * Sets the basis of alignment during the layout of this ViewGroup. + * Valid values are either {@link #LAYOUT_MODE_CLIP_BOUNDS} or + * {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. + * + * @param mode the layout mode to use during layout operations + * + * @see #getLayoutMode(ViewGroup) + */ + public static void setLayoutMode(ViewGroup group, int mode) { + IMPL.setLayoutMode(group, mode); + } + + /** + * Changes whether or not this ViewGroup should be treated as a single entity during + * Activity Transitions. + * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit + * in Activity transitions. If false, the ViewGroup won't transition, + * only its children. If true, the entire ViewGroup will transition + * together. + */ + public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + IMPL.setTransitionGroup(group, isTransitionGroup); + } + + /** + * Returns true if this ViewGroup should be considered as a single entity for removal + * when executing an Activity transition. If this is false, child elements will move + * individually during the transition. + */ + public static boolean isTransitionGroup(ViewGroup group) { + return IMPL.isTransitionGroup(group); + } + + /** + * Return the current axes of nested scrolling for this ViewGroup. + * + *

    A ViewGroup returning something other than {@link ViewCompat#SCROLL_AXIS_NONE} is + * currently acting as a nested scrolling parent for one or more descendant views in + * the hierarchy.

    + * + * @return Flags indicating the current axes of nested scrolling + * @see ViewCompat#SCROLL_AXIS_HORIZONTAL + * @see ViewCompat#SCROLL_AXIS_VERTICAL + * @see ViewCompat#SCROLL_AXIS_NONE + */ + public static int getNestedScrollAxes(ViewGroup group) { + return IMPL.getNestedScrollAxes(group); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java new file mode 100644 index 00000000000..96349edb889 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatHC { + private ViewGroupCompatHC() { + } + + public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + group.setMotionEventSplittingEnabled(split); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java new file mode 100644 index 00000000000..73de780023e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * ICS specific ViewGroup API implementation. + */ +class ViewGroupCompatIcs { + public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event) { + return group.onRequestSendAccessibilityEvent(child, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java new file mode 100644 index 00000000000..086b07004f8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatJellybeanMR2 { + public static int getLayoutMode(ViewGroup group) { + return group.getLayoutMode(); + } + + public static void setLayoutMode(ViewGroup group, int mode) { + group.setLayoutMode(mode); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java new file mode 100644 index 00000000000..1a62404373c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatLollipop { + + public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + group.setTransitionGroup(isTransitionGroup); + } + + public static boolean isTransitionGroup(ViewGroup group) { + return group.isTransitionGroup(); + } + + public static int getNestedScrollAxes(ViewGroup group) { + return group.getNestedScrollAxes(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java new file mode 100644 index 00000000000..57152c2f0f2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java @@ -0,0 +1,2976 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.DrawableRes; +import android.support.v4.os.ParcelableCompat; +import android.support.v4.os.ParcelableCompatCreatorCallbacks; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Layout manager that allows the user to flip left and right + * through pages of data. You supply an implementation of a + * {@link PagerAdapter} to generate the pages that the view shows. + * + *

    Note this class is currently under early design and + * development. The API will likely change in later updates of + * the compatibility library, requiring changes to the source code + * of apps when they are compiled against the newer version.

    + * + *

    ViewPager is most often used in conjunction with {@link android.app.Fragment}, + * which is a convenient way to supply and manage the lifecycle of each page. + * There are standard adapters implemented for using fragments with the ViewPager, + * which cover the most common use cases. These are + * {@link android.support.v4.app.FragmentPagerAdapter} and + * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these + * classes have simple code showing how to build a full user interface + * with them. + * + *

    For more information about how to use ViewPager, read Creating Swipe Views with + * Tabs.

    + * + *

    Below is a more complicated example of ViewPager, using it in conjunction + * with {@link android.app.ActionBar} tabs. You can find other examples of using + * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java + * complete} + */ +public class ViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator COMPARATOR = new Comparator(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList mItems = new ArrayList(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + private PagerAdapter mAdapter; + private int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffectCompat mLeftEdge; + private EdgeEffectCompat mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private List mOnPageChangeListeners; + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private OnAdapterChangeListener mAdapterChangeListener; + private PageTransformer mPageTransformer; + private Method mSetChildrenDrawingOrderEnabled; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + public void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see ViewPager#SCROLL_STATE_IDLE + * @see ViewPager#SCROLL_STATE_DRAGGING + * @see ViewPager#SCROLL_STATE_SETTLING + */ + public void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + *

    As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.

    + */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + public void transformPage(View page, float position); + } + + /** + * Used internally to monitor when adapters are switched. + */ + interface OnAdapterChangeListener { + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); + } + + /** + * Used internally to tag special types of child views that should be added as + * pager decorations by default. + */ + interface Decor {} + + public ViewPager(Context context) { + super(context); + initViewPager(); + } + + public ViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffectCompat(context); + mRightEdge = new EdgeEffectCompat(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + super.onDetachedFromWindow(); + } + + private void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + dispatchOnScrollStateChanged(newState); + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + if (mAdapterChangeListener != null && oldAdapter != adapter) { + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public PagerAdapter getAdapter() { + return mAdapter; + } + + void setOnAdapterChangeListener(OnAdapterChangeListener listener) { + mAdapterChangeListener = listener; + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i=0; iComponents that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

    + * + * @param listener listener to add + */ + public void addOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + /** + * Set a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + *

    Note: Prior to Android 3.0 the property animation APIs did not exist. + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

    + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + if (Build.VERSION.SDK_INT >= 11) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabledCompat(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + } + + void setChildrenDrawingOrderEnabledCompat(boolean enable) { + if (Build.VERSION.SDK_INT >= 7) { + if (mSetChildrenDrawingOrderEnabled == null) { + try { + mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( + "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); + } + } + try { + mSetChildrenDrawingOrderEnabled.invoke(this, enable); + } catch (Exception e) { + Log.e(TAG, "Error changing children drawing order", e); + } + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + *

    This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.

    + * + *

    You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.

    + * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(getContext().getResources().getDrawable(resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + int sx = getScrollX(); + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && + mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + int focusDirection = View.FOCUS_FORWARD; + if (mCurItem != newCurrentItem) { + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N-1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i=0; i(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 ? + curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends BaseSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Parcelable.Creator CREATOR + = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }); + + SavedState(Parcel in, ClassLoader loader) { + super(in); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + lp.isDecor |= child instanceof Decor; + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i=0; i 0 && !mItems.isEmpty()) { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + if (!mScroller.isFinished()) { + // We now return to your regularly scheduled scroll, already in progress. + final int newDuration = mScroller.getDuration() - mScroller.timePassed(); + ItemInfo targetInfo = infoForPosition(mCurItem); + mScroller.startScroll(newOffsetPixels, 0, + (int) (targetInfo.offset * width), 0, newDuration); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = (int) (scrollOffset * + (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) / + (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + dispatchOnPageScrolled(position, offset, offsetPixels); + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrolled(position, offset, offsetPixels); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(position); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void dispatchOnScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrollStateChanged(state); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrollStateChanged(state); + } + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (x != oldX) { + pageScrolled(x); + } + } + } + mPopulatePending = false; + for (int i=0; i 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable ? + ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; + ViewCompat.setLayerType(getChildAt(i), layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + mIsBeingDragged = false; + mIsUnableToDrag = false; + mActivePointerId = INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && + canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsUnableToDrag = false; + + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING && + Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = MotionEventCompat.findPointerIndex( + ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int activePointerIndex = + MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + final float x = MotionEventCompat.getX(ev, index); + mLastMotionX = x; + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = MotionEventCompat.getX(ev, + MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = (int) (currentPage + pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = ViewCompat.getOverScrollMode(this); + if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, + (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + *

    A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + *

    During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && ViewCompat.canScrollHorizontally(v, -dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + case KeyEvent.KEYCODE_TAB: + if (Build.VERSION.SDK_INT >= 11) { + // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD + // before Android 3.0. Ignore the tab key on those devices. + if (KeyEventCompat.hasNoModifiers(event)) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + } + break; + } + } + return handled; + } + + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem-1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { + setCurrentItem(mCurItem+1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if ( + descendantFocusability != FOCUS_AFTER_DESCENDANTS || + // No focusable descendants + (focusableCount == views.size())) { + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem && + child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(ViewPager.class.getName()); + final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); + recordCompat.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED + && mAdapter != null) { + recordCompat.setItemCount(mAdapter.getCount()); + recordCompat.setFromIndex(mCurItem); + recordCompat.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link android.view.Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java new file mode 100644 index 00000000000..b95b332fabb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java @@ -0,0 +1,427 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.os.Build; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +/** + * Helper for accessing features in {@link ViewParent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewParentCompat { + + interface ViewParentCompatImpl { + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event); + boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes); + void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes); + void onStopNestedScroll(ViewParent parent, View target); + void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed); + boolean onNestedFling(ViewParent parent, View target, float velocityX, float velocityY, + boolean consumed); + boolean onNestedPreFling(ViewParent parent, View target, float velocityX, float velocityY); + void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType); + } + + static class ViewParentCompatStubImpl implements ViewParentCompatImpl { + @Override + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + // Emulate what ViewRootImpl does in ICS and above. + if (child == null) { + return false; + } + final AccessibilityManager manager = (AccessibilityManager) child.getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + manager.sendAccessibilityEvent(event); + return true; + } + + @Override + public boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onStartNestedScroll(child, target, + nestedScrollAxes); + } + return false; + } + + @Override + public void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target, + nestedScrollAxes); + } + } + + @Override + public void onStopNestedScroll(ViewParent parent, View target) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onStopNestedScroll(target); + } + } + + @Override + public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); + } + } + + @Override + public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); + } + } + + @Override + public boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY, + consumed); + } + return false; + } + + @Override + public boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onNestedPreFling(target, velocityX, + velocityY); + } + return false; + } + + @Override + public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + } + } + + static class ViewParentCompatICSImpl extends ViewParentCompatStubImpl { + @Override + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return ViewParentCompatICS.requestSendAccessibilityEvent(parent, child, event); + } + } + + static class ViewParentCompatKitKatImpl extends ViewParentCompatICSImpl { + + @Override + public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + ViewParentCompatKitKat.notifySubtreeAccessibilityStateChanged(parent, child, + source, changeType); + } + } + + static class ViewParentCompatLollipopImpl extends ViewParentCompatKitKatImpl { + @Override + public boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target, + nestedScrollAxes); + } + + @Override + public void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + ViewParentCompatLollipop.onNestedScrollAccepted(parent, child, target, + nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(ViewParent parent, View target) { + ViewParentCompatLollipop.onStopNestedScroll(parent, target); + } + + @Override + public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + ViewParentCompatLollipop.onNestedScroll(parent, target, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); + } + + @Override + public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + ViewParentCompatLollipop.onNestedPreScroll(parent, target, dx, dy, consumed); + } + + @Override + public boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + return ViewParentCompatLollipop.onNestedFling(parent, target, velocityX, velocityY, + consumed); + } + + @Override + public boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + return ViewParentCompatLollipop.onNestedPreFling(parent, target, velocityX, velocityY); + } + } + + static final ViewParentCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new ViewParentCompatLollipopImpl(); + } else if (version >= 19) { + IMPL = new ViewParentCompatKitKatImpl(); + } else if (version >= 14) { + IMPL = new ViewParentCompatICSImpl(); + } else { + IMPL = new ViewParentCompatStubImpl(); + } + } + + /* + * Hide the constructor. + */ + private ViewParentCompat() { + + } + + /** + * Called by a child to request from its parent to send an {@link AccessibilityEvent}. + * The child has already populated a record for itself in the event and is delegating + * to its parent to send the event. The parent can optionally add a record for itself. + *

    + * Note: An accessibility event is fired by an individual view which populates the + * event with a record for its state and requests from its parent to perform + * the sending. The parent can optionally add a record for itself before + * dispatching the request to its parent. A parent can also choose not to + * respect the request for sending the event. The accessibility event is sent + * by the topmost view in the view tree.

    + * + * @param parent The parent whose method to invoke. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event was sent. + */ + public static boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return IMPL.requestSendAccessibilityEvent(parent, child, event); + } + + /** + * React to a descendant view initiating a nestable scroll operation, claiming the + * nested scroll operation if appropriate. + * + *

    This method will be called in response to a descendant view invoking + * {@link ViewCompat#startNestedScroll(View, int)}. Each parent up the view hierarchy will be + * given an opportunity to respond and claim the nested scrolling operation by returning + * true.

    + * + *

    This method may be overridden by ViewParent implementations to indicate when the view + * is willing to support a nested scrolling operation that is about to begin. If it returns + * true, this ViewParent will become the target view's nested scrolling parent for the duration + * of the scroll operation in progress. When the nested scroll is finished this ViewParent + * will receive a call to {@link #onStopNestedScroll(ViewParent, View)}. + *

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @return true if this ViewParent accepts the nested scroll operation + */ + public static boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); + } + + /** + * React to the successful claiming of a nested scroll operation. + * + *

    This method will be called after + * {@link #onStartNestedScroll(ViewParent, View, View, int) onStartNestedScroll} returns true. + * It offers an opportunity for the view and its superclasses to perform initial configuration + * for the nested scroll. Implementations of this method should always call their superclass's + * implementation of this method if one is present.

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @see #onStartNestedScroll(ViewParent, View, View, int) + * @see #onStopNestedScroll(ViewParent, View) + */ + public static void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + IMPL.onNestedScrollAccepted(parent, child, target, nestedScrollAxes); + } + + /** + * React to a nested scroll operation ending. + * + *

    Perform cleanup after a nested scrolling operation. + * This method will be called when a nested scroll stops, for example when a nested touch + * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event. + * Implementations of this method should always call their superclass's implementation of this + * method if one is present.

    + * + * @param target View that initiated the nested scroll + */ + public static void onStopNestedScroll(ViewParent parent, View target) { + IMPL.onStopNestedScroll(parent, target); + } + + /** + * React to a nested scroll in progress. + * + *

    This method will be called when the ViewParent's current nested scrolling child view + * dispatches a nested scroll event. To receive calls to this method the ViewParent must have + * previously returned true for a call to + * {@link #onStartNestedScroll(ViewParent, View, View, int)}.

    + * + *

    Both the consumed and unconsumed portions of the scroll distance are reported to the + * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll + * position of multiple child elements, for example. The unconsumed portion may be used to + * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling + * a list within a vertical drawer where the drawer begins dragging once the edge of inner + * scrolling content is reached.

    + * + * @param target The descendent view controlling the nested scroll + * @param dxConsumed Horizontal scroll distance in pixels already consumed by target + * @param dyConsumed Vertical scroll distance in pixels already consumed by target + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target + * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target + */ + public static void onNestedScroll(ViewParent parent, View target, int dxConsumed, + int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + IMPL.onNestedScroll(parent, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + } + + /** + * React to a nested scroll in progress before the target view consumes a portion of the scroll. + * + *

    When working with nested scrolling often the parent view may want an opportunity + * to consume the scroll before the nested scrolling child does. An example of this is a + * drawer that contains a scrollable list. The user will want to be able to scroll the list + * fully into view before the list itself begins scrolling.

    + * + *

    onNestedPreScroll is called when a nested scrolling child invokes + * {@link ViewCompat#dispatchNestedPreScroll(View, int, int, int[], int[])}. The implementation + * should report how any pixels of the scroll reported by dx, dy were consumed in the + * consumed array. Index 0 corresponds to dx and index 1 corresponds to dy. + * This parameter will never be null. Initial values for consumed[0] and consumed[1] + * will always be 0.

    + * + * @param target View that initiated the nested scroll + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent + */ + public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + IMPL.onNestedPreScroll(parent, target, dx, dy, consumed); + } + + /** + * Request a fling from a nested scroll. + * + *

    This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling + */ + public static boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + return IMPL.onNestedFling(parent, target, velocityX, velocityY, consumed); + } + + /** + * React to a nested fling before the target view consumes it. + * + *

    This method siginfies that a nested scrolling child has detected a fling with the given + * velocity along each axis. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling parent is consuming motion as part of a + * {@link #onNestedPreScroll(ViewParent, View, int, int, int[]) pre-scroll}, it may be + * appropriate for it to also consume the pre-fling to complete that same motion. By returning + * true from this method, the parent indicates that the child should not + * fling its own internal content as well.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @return true if this parent consumed the fling ahead of the target view + */ + public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + return IMPL.onNestedPreFling(parent, target, velocityX, velocityY); + } + + /** + * Notifies a view parent that the accessibility state of one of its + * descendants has changed and that the structure of the subtree is + * different. + * @param child The direct child whose subtree has changed. + * @param source The descendant view that changed. + * @param changeType A bit mask of the types of changes that occurred. One + * or more of: + *
      + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + *
    + */ + public static void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + IMPL.notifySubtreeAccessibilityStateChanged(parent, child, source, changeType); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java new file mode 100644 index 00000000000..251de0e0d08 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +/** + * ICS-specific ViewParent API implementation. + */ +public class ViewParentCompatICS { + public static boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return parent.requestSendAccessibilityEvent(child, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java new file mode 100644 index 00000000000..5e60a9bb0e4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java @@ -0,0 +1,27 @@ +/* + * 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.v4.view; + +import android.view.View; +import android.view.ViewParent; + +class ViewParentCompatKitKat { + public static void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + parent.notifySubtreeAccessibilityStateChanged(child, source, changeType); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java new file mode 100644 index 00000000000..7dbcf61b6d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java @@ -0,0 +1,98 @@ +/* + * 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.v4.view; + +import android.util.Log; +import android.view.View; +import android.view.ViewParent; + +class ViewParentCompatLollipop { + private static final String TAG = "ViewParentCompat"; + + public static boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + try { + return parent.onStartNestedScroll(child, target, nestedScrollAxes); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onStartNestedScroll", e); + return false; + } + } + + public static void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + try { + parent.onNestedScrollAccepted(child, target, nestedScrollAxes); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedScrollAccepted", e); + } + } + + public static void onStopNestedScroll(ViewParent parent, View target) { + try { + parent.onStopNestedScroll(target); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onStopNestedScroll", e); + } + } + + public static void onNestedScroll(ViewParent parent, View target, int dxConsumed, + int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + try { + parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedScroll", e); + } + } + + public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + try { + parent.onNestedPreScroll(target, dx, dy, consumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedPreScroll", e); + } + } + + public static boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + try { + return parent.onNestedFling(target, velocityX, velocityY, consumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedFling", e); + return false; + } + } + + public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + try { + return parent.onNestedPreFling(target, velocityX, velocityY); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedPreFling", e); + return false; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java new file mode 100644 index 00000000000..c4a21454b59 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java @@ -0,0 +1,1338 @@ +/* + * 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. + */ +package android.support.v4.view; + +import android.view.View; +import android.view.animation.Interpolator; + +import java.lang.ref.WeakReference; +import java.util.WeakHashMap; + +public class ViewPropertyAnimatorCompat { + private static final String TAG = "ViewAnimatorCompat"; + private WeakReference mView; + private Runnable mStartAction = null; + private Runnable mEndAction = null; + private int mOldLayerType = -1; + // HACK ALERT! Choosing this id knowing that the framework does not use it anywhere + // internally and apps should use ids higher than it + static final int LISTENER_TAG_ID = 0x7e000000; + + + ViewPropertyAnimatorCompat(View view) { + mView = new WeakReference(view); + } + + interface ViewPropertyAnimatorCompatImpl { + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value); + public long getDuration(ViewPropertyAnimatorCompat vpa, View view); + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value); + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view); + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value); + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view); + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value); + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void cancel(ViewPropertyAnimatorCompat vpa, View view); + public void x(ViewPropertyAnimatorCompat vpa, View view, float value); + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void y(ViewPropertyAnimatorCompat vpa, View view, float value); + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void z(ViewPropertyAnimatorCompat vpa, View view, float value); + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void start(ViewPropertyAnimatorCompat vpa, View view); + public void withLayer(ViewPropertyAnimatorCompat vpa, View view); + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable); + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable); + public void setListener(ViewPropertyAnimatorCompat vpa, View view, + ViewPropertyAnimatorListener listener); + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, + ViewPropertyAnimatorUpdateListener listener); + }; + + static class BaseViewPropertyAnimatorCompatImpl implements ViewPropertyAnimatorCompatImpl { + WeakHashMap mStarterMap = null; + + @Override + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value) { + // noop on versions prior to ICS + } + + @Override + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + vpa.mEndAction = runnable; + postStartMessage(vpa, view); + } + + @Override + public long getDuration(ViewPropertyAnimatorCompat vpa, View view) { + return 0; + } + + @Override + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value) { + // noop on versions prior to ICS + } + + @Override + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view) { + return null; + } + + @Override + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value) { + // noop on versions prior to ICS + } + + @Override + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view) { + return 0; + } + + @Override + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void cancel(ViewPropertyAnimatorCompat vpa, View view) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void x(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void y(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void z(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void start(ViewPropertyAnimatorCompat vpa, View view) { + removeStartMessage(view); + startAnimation(vpa, view); + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + // noop on versions prior to ICS + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + vpa.mStartAction = runnable; + postStartMessage(vpa, view); + } + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + view.setTag(LISTENER_TAG_ID, listener); + } + + @Override + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorUpdateListener listener) { + // noop + } + + private void startAnimation(ViewPropertyAnimatorCompat vpa, View view) { + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + Runnable startAction = vpa.mStartAction; + Runnable endAction = vpa.mEndAction; + if (startAction != null) { + startAction.run(); + } + if (listener != null) { + listener.onAnimationStart(view); + listener.onAnimationEnd(view); + } + if (endAction != null) { + endAction.run(); + } + if (mStarterMap != null) { + mStarterMap.remove(view); + } + } + + class Starter implements Runnable { + WeakReference mViewRef; + ViewPropertyAnimatorCompat mVpa; + + private Starter(ViewPropertyAnimatorCompat vpa, View view) { + mViewRef = new WeakReference(view); + mVpa = vpa; + } + + @Override + public void run() { + final View view = mViewRef.get(); + if (view != null) { + startAnimation(mVpa, view); + } + } + }; + + private void removeStartMessage(View view) { + Runnable starter = null; + if (mStarterMap != null) { + starter = mStarterMap.get(view); + if (starter != null) { + view.removeCallbacks(starter); + } + } + } + + private void postStartMessage(ViewPropertyAnimatorCompat vpa, View view) { + Runnable starter = null; + if (mStarterMap != null) { + starter = mStarterMap.get(view); + } + if (starter == null) { + starter = new Starter(vpa, view); + if (mStarterMap == null) { + mStarterMap = new WeakHashMap(); + } + mStarterMap.put(view, starter); + } + view.removeCallbacks(starter); + view.post(starter); + } + + } + + static class ICSViewPropertyAnimatorCompatImpl extends BaseViewPropertyAnimatorCompatImpl { + WeakHashMap mLayerMap = null; + + @Override + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value) { + ViewPropertyAnimatorCompatICS.setDuration(view, value); + } + + @Override + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.alpha(view, value); + } + + @Override + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationX(view, value); + } + + @Override + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationY(view, value); + } + + @Override + public long getDuration(ViewPropertyAnimatorCompat vpa, View view) { + return ViewPropertyAnimatorCompatICS.getDuration(view); + } + + @Override + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value) { + ViewPropertyAnimatorCompatICS.setInterpolator(view, value); + } + + @Override + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value) { + ViewPropertyAnimatorCompatICS.setStartDelay(view, value); + } + + @Override + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view) { + return ViewPropertyAnimatorCompatICS.getStartDelay(view); + } + + @Override + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.alphaBy(view, value); + } + + @Override + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotation(view, value); + } + + @Override + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationBy(view, value); + } + + @Override + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationX(view, value); + } + + @Override + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationXBy(view, value); + } + + @Override + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationY(view, value); + } + + @Override + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationYBy(view, value); + } + + @Override + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleX(view, value); + } + + @Override + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleXBy(view, value); + } + + @Override + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleY(view, value); + } + + @Override + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleYBy(view, value); + } + + @Override + public void cancel(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatICS.cancel(view); + } + + @Override + public void x(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.x(view, value); + } + + @Override + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.xBy(view, value); + } + + @Override + public void y(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.y(view, value); + } + + @Override + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.yBy(view, value); + } + + @Override + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationXBy(view, value); + } + + @Override + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationYBy(view, value); + } + + @Override + public void start(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatICS.start(view); + } + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + view.setTag(LISTENER_TAG_ID, listener); + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, final Runnable runnable) { + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + vpa.mEndAction = runnable; + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, final Runnable runnable) { + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + vpa.mStartAction = runnable; + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + vpa.mOldLayerType = ViewCompat.getLayerType(view); + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + } + + static class MyVpaListener implements ViewPropertyAnimatorListener { + + ViewPropertyAnimatorCompat mVpa; + + MyVpaListener(ViewPropertyAnimatorCompat vpa) { + mVpa = vpa; + } + + @Override + public void onAnimationStart(View view) { + if (mVpa.mOldLayerType >= 0) { + ViewCompat.setLayerType(view, ViewCompat.LAYER_TYPE_HARDWARE, null); + } + if (mVpa.mStartAction != null) { + mVpa.mStartAction.run(); + } + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationStart(view); + } + } + + @Override + public void onAnimationEnd(View view) { + if (mVpa.mOldLayerType >= 0) { + ViewCompat.setLayerType(view, mVpa.mOldLayerType, null); + mVpa.mOldLayerType = -1; + } + if (mVpa.mEndAction != null) { + mVpa.mEndAction.run(); + } + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationEnd(view); + } + } + + @Override + public void onAnimationCancel(View view) { + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationCancel(view); + } + } + }; + } + + static class JBViewPropertyAnimatorCompatImpl extends ICSViewPropertyAnimatorCompatImpl { + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + ViewPropertyAnimatorCompatJB.setListener(view, listener); + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + ViewPropertyAnimatorCompatJB.withStartAction(view, runnable); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + ViewPropertyAnimatorCompatJB.withEndAction(view, runnable); + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatJB.withLayer(view); + } + } + + static class JBMr2ViewPropertyAnimatorCompatImpl extends JBViewPropertyAnimatorCompatImpl { + + @Override + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view) { + return (Interpolator) ViewPropertyAnimatorCompatJellybeanMr2.getInterpolator(view); + } + } + + static class KitKatViewPropertyAnimatorCompatImpl extends JBMr2ViewPropertyAnimatorCompatImpl { + @Override + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorUpdateListener listener) { + ViewPropertyAnimatorCompatKK.setUpdateListener(view, listener); + } + } + + static class LollipopViewPropertyAnimatorCompatImpl extends KitKatViewPropertyAnimatorCompatImpl { + @Override + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.translationZ(view, value); + } + + @Override + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.translationZBy(view, value); + } + + @Override + public void z(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.z(view, value); + } + + @Override + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.zBy(view, value); + } + } + + static final ViewPropertyAnimatorCompatImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LollipopViewPropertyAnimatorCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewPropertyAnimatorCompatImpl(); + } else if (version >= 18) { + IMPL = new JBMr2ViewPropertyAnimatorCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewPropertyAnimatorCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewPropertyAnimatorCompatImpl(); + } else { + IMPL = new BaseViewPropertyAnimatorCompatImpl(); + } + } + + /** + * Sets the duration for the underlying animator that animates the requested properties. + * By default, the animator uses the default value for ValueAnimator. Calling this method + * will cause the declared value to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The length of ensuing property animations, in milliseconds. The value + * cannot be negative. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setDuration(long value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setDuration(this, view, value); + } + return this; + } + + /** + * This method will cause the View's alpha property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat alpha(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.alpha(this, view, value); + } + return this; + } + + /** + * This method will cause the View's alpha property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat alphaBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.alphaBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationY(this, view, value); + } + return this; + } + + /** + * Specifies an action to take place when the next animation ends. The action is only + * run if the animation ends normally; if the ViewPropertyAnimator is canceled during + * that animation, the runnable will not run. + * This method, along with {@link #withStartAction(Runnable)}, is intended to help facilitate + * choreographing ViewPropertyAnimator animations with other animations or actions + * in the application. + * + *

    For example, the following code animates a view to x=200 and then back to 0:

    + *
    +     *     Runnable endAction = new Runnable() {
    +     *         public void run() {
    +     *             view.animate().x(0);
    +     *         }
    +     *     };
    +     *     view.animate().x(200).withEndAction(endAction);
    +     * 
    + * + *

    Prior to API 14, this method will run the action immediately.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object and running the action + * in that listener's {@link ViewPropertyAnimatorListener#onAnimationEnd(View)} method.

    + * + * @param runnable The action to run when the next animation ends. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withEndAction(Runnable runnable) { + View view; + if ((view = mView.get()) != null) { + IMPL.withEndAction(this, view, runnable); + } + return this; + } + + /** + * Returns the current duration of property animations. If the duration was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + *

    Prior to API 14, this method will return 0.

    + * + * @see #setDuration(long) + * @return The duration of animations, in milliseconds. + */ + public long getDuration() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getDuration(this, view); + } else { + return 0; + } + } + + /** + * Sets the interpolator for the underlying animator that animates the requested properties. + * By default, the animator uses the default interpolator for ValueAnimator. Calling this method + * will cause the declared object to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The TimeInterpolator to be used for ensuing property animations. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setInterpolator(Interpolator value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setInterpolator(this, view, value); + } + return this; + } + + /** + * Returns the timing interpolator that this animation uses. + * + *

    Prior to API 14, this method will return null.

    + * + * @return The timing interpolator for this animation. + */ + public Interpolator getInterpolator() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getInterpolator(this, view); + } + else return null; + } + + /** + * Sets the startDelay for the underlying animator that animates the requested properties. + * By default, the animator uses the default value for ValueAnimator. Calling this method + * will cause the declared value to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The delay of ensuing property animations, in milliseconds. The value + * cannot be negative. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setStartDelay(long value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setStartDelay(this, view, value); + } + return this; + } + + /** + * Returns the current startDelay of property animations. If the startDelay was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + *

    Prior to API 14, this method will return 0.

    + * + * @see #setStartDelay(long) + * @return The startDelay of animations, in milliseconds. + */ + public long getStartDelay() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getStartDelay(this, view); + } else { + return 0; + } + } + + /** + * This method will cause the View's rotation property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotation(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotation(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotation property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationY(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationYBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleY(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleYBy(this, view, value); + } + return this; + } + + /** + * Cancels all property animations that are currently running or pending. + */ + public void cancel() { + View view; + if ((view = mView.get()) != null) { + IMPL.cancel(this, view); + } + } + + /** + * This method will cause the View's x property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat x(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.x(this, view, value); + } + return this; + } + + /** + * This method will cause the View's x property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat xBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.xBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's y property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat y(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.y(this, view, value); + } + return this; + } + + /** + * This method will cause the View's y property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat yBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.yBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationYBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationZ property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationZBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationZBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationZ property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationZ(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationZ(this, view, value); + } + return this; + } + + /** + * This method will cause the View's z property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat z(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.z(this, view, value); + } + return this; + } + + /** + * This method will cause the View's z property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat zBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.zBy(this, view, value); + } + return this; + } + + /** + * Starts the currently pending property animations immediately. Calling start() + * is optional because all animations start automatically at the next opportunity. However, + * if the animations are needed to start immediately and synchronously (not at the time when + * the next event is processed by the hierarchy, which is when the animations would begin + * otherwise), then this method can be used. + * + *

    Prior to API 14, this method will do nothing.

    + */ + public void start() { + View view; + if ((view = mView.get()) != null) { + IMPL.start(this, view); + } + } + + /** + * The View associated with this ViewPropertyAnimator will have its + * {@link ViewCompat#setLayerType(View, int, android.graphics.Paint) layer type} set to + * {@link ViewCompat#LAYER_TYPE_HARDWARE} for the duration of the next animation. + * As stated in the documentation for {@link ViewCompat#LAYER_TYPE_HARDWARE}, + * the actual type of layer used internally depends on the runtime situation of the + * view. If the activity and this view are hardware-accelerated, then the layer will be + * accelerated as well. If the activity or the view is not accelerated, then the layer will + * effectively be the same as {@link ViewCompat#LAYER_TYPE_SOFTWARE}. + * + *

    This state is not persistent, either on the View or on this ViewPropertyAnimator: the + * layer type of the View will be restored when the animation ends to what it was when this + * method was called, and this setting on ViewPropertyAnimator is only valid for the next + * animation. Note that calling this method and then independently setting the layer type of + * the View (by a direct call to + * {@link ViewCompat#setLayerType(View, int, android.graphics.Paint)}) will result in some + * inconsistency, including having the layer type restored to its pre-withLayer() + * value when the animation ends.

    + * + *

    Prior to API 14, this method will do nothing.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object, setting a hardware layer in + * the listener's {@link ViewPropertyAnimatorListener#onAnimationStart(View)} method, + * and then restoring the orignal layer type in the listener's + * {@link ViewPropertyAnimatorListener#onAnimationEnd(View)} method.

    + * + * @see View#setLayerType(int, android.graphics.Paint) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withLayer() { + View view; + if ((view = mView.get()) != null) { + IMPL.withLayer(this, view); + } + return this; + } + + /** + * Specifies an action to take place when the next animation runs. If there is a + * {@link #setStartDelay(long) startDelay} set on this ViewPropertyAnimator, then the + * action will run after that startDelay expires, when the actual animation begins. + * This method, along with {@link #withEndAction(Runnable)}, is intended to help facilitate + * choreographing ViewPropertyAnimator animations with other animations or actions + * in the application. + * + *

    Prior to API 14, this method will run the action immediately.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object and running the action + * in that listener's {@link ViewPropertyAnimatorListener#onAnimationStart(View)} method.

    + * + * @param runnable The action to run when the next animation starts. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withStartAction(Runnable runnable) { + View view; + if ((view = mView.get()) != null) { + IMPL.withStartAction(this, view, runnable); + } + return this; + } + + /** + * Sets a listener for events in the underlying Animators that run the property + * animations. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param listener The listener to be called with AnimatorListener events. A value of + * null removes any existing listener. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setListener(ViewPropertyAnimatorListener listener) { + View view; + if ((view = mView.get()) != null) { + IMPL.setListener(this, view, listener); + } + return this; + } + + /** + * Sets a listener for update events in the underlying Animator that runs + * the property animations. + * + *

    Prior to API 19, this method will do nothing.

    + * + * @param listener The listener to be called with update events. A value of + * null removes any existing listener. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setUpdateListener( + ViewPropertyAnimatorUpdateListener listener) { + View view; + if ((view = mView.get()) != null) { + IMPL.setUpdateListener(this, view, listener); + } + return this; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java new file mode 100644 index 00000000000..1fd7f3ebc9f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java @@ -0,0 +1,156 @@ +/* + * 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. + */ +package android.support.v4.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.view.View; +import android.view.animation.Interpolator; + +class ViewPropertyAnimatorCompatICS { + + public static void setDuration(View view, long value) { + view.animate().setDuration(value); + } + + public static void alpha(View view, float value) { + view.animate().alpha(value); + } + + public static void translationX(View view, float value) { + view.animate().translationX(value); + } + + public static void translationY(View view, float value) { + view.animate().translationY(value); + } + + public static long getDuration(View view) { + return view.animate().getDuration(); + } + + public static void setInterpolator(View view, Interpolator value) { + view.animate().setInterpolator(value); + } + + public static void setStartDelay(View view, long value) { + view.animate().setStartDelay(value); + } + + public static long getStartDelay(View view) { + return view.animate().getStartDelay(); + } + + public static void alphaBy(View view, float value) { + view.animate().alphaBy(value); + } + + public static void rotation(View view, float value) { + view.animate().rotation(value); + } + + public static void rotationBy(View view, float value) { + view.animate().rotationBy(value); + } + + public static void rotationX(View view, float value) { + view.animate().rotationX(value); + } + + public static void rotationXBy(View view, float value) { + view.animate().rotationXBy(value); + } + + public static void rotationY(View view, float value) { + view.animate().rotationY(value); + } + + public static void rotationYBy(View view, float value) { + view.animate().rotationYBy(value); + } + + public static void scaleX(View view, float value) { + view.animate().scaleX(value); + } + + public static void scaleXBy(View view, float value) { + view.animate().scaleXBy(value); + } + + public static void scaleY(View view, float value) { + view.animate().scaleY(value); + } + + public static void scaleYBy(View view, float value) { + view.animate().scaleYBy(value); + } + + public static void cancel(View view) { + view.animate().cancel(); + } + + public static void x(View view, float value) { + view.animate().x(value); + } + + public static void xBy(View view, float value) { + view.animate().xBy(value); + } + + public static void y(View view, float value) { + view.animate().y(value); + } + + public static void yBy(View view, float value) { + view.animate().yBy(value); + } + + public static void translationXBy(View view, float value) { + view.animate().translationXBy(value); + } + + public static void translationYBy(View view, float value) { + view.animate().translationYBy(value); + } + + public static void start(View view) { + view.animate().start(); + } + + public static void setListener(final View view, + final ViewPropertyAnimatorListener listener) { + if (listener != null) { + view.animate().setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + listener.onAnimationCancel(view); + } + + @Override + public void onAnimationEnd(Animator animation) { + listener.onAnimationEnd(view); + } + + @Override + public void onAnimationStart(Animator animation) { + listener.onAnimationStart(view); + } + }); + } else { + view.animate().setListener(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java new file mode 100644 index 00000000000..6107c42c4f4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java @@ -0,0 +1,59 @@ +/* + * 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. + */ +package android.support.v4.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.view.View; + +class ViewPropertyAnimatorCompatJB { + + public static void withStartAction(View view, Runnable runnable) { + view.animate().withStartAction(runnable); + } + + public static void withEndAction(View view, Runnable runnable) { + view.animate().withEndAction(runnable); + } + + public static void withLayer(View view) { + view.animate().withLayer(); + } + + public static void setListener(final View view, + final ViewPropertyAnimatorListener listener) { + if (listener != null) { + view.animate().setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + listener.onAnimationCancel(view); + } + + @Override + public void onAnimationEnd(Animator animation) { + listener.onAnimationEnd(view); + } + + @Override + public void onAnimationStart(Animator animation) { + listener.onAnimationStart(view); + } + }); + } else { + view.animate().setListener(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java new file mode 100644 index 00000000000..e9a29ee0127 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java @@ -0,0 +1,25 @@ +/* + * 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. + */ +package android.support.v4.view; + +import android.view.View; +import android.view.animation.Interpolator; + +class ViewPropertyAnimatorCompatJellybeanMr2 { + public static Interpolator getInterpolator(View view) { + return (Interpolator) view.animate().getInterpolator(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java new file mode 100644 index 00000000000..5439972d655 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +package android.support.v4.view; + +import android.animation.ValueAnimator; +import android.view.View; + +class ViewPropertyAnimatorCompatKK { + + public static void setUpdateListener(final View view, + final ViewPropertyAnimatorUpdateListener listener) { + view.animate().setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + listener.onAnimationUpdate(view); + } + }); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java new file mode 100644 index 00000000000..3bfc427e77a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java @@ -0,0 +1,39 @@ +/* + * 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.v4.view; + +import android.view.View; + +class ViewPropertyAnimatorCompatLollipop { + + public static void translationZ(View view, float value) { + view.animate().translationZ(value); + } + + public static void translationZBy(View view, float value) { + view.animate().translationZBy(value); + } + + public static void z(View view, float value) { + view.animate().z(value); + } + + public static void zBy(View view, float value) { + view.animate().zBy(value); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java new file mode 100644 index 00000000000..72f1daf9c23 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java @@ -0,0 +1,47 @@ +package android.support.v4.view;/* + * 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. + */ + +import android.view.View; + +/** + *

    An animation listener receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * start of the animation.

    + */ +public interface ViewPropertyAnimatorListener { + /** + *

    Notifies the start of the animation.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationStart(View view); + + /** + *

    Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationEnd(View view); + + /** + *

    Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationCancel(View view); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java new file mode 100644 index 00000000000..4f58912824e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * This adapter class provides empty implementations of the methods from + * {@link ViewPropertyAnimatorListener}. Any custom listener that cares only about a subset of + * the methods of this listener can simply subclass this adapter class instead of implementing + * the interface directly. + */ +public class ViewPropertyAnimatorListenerAdapter implements ViewPropertyAnimatorListener { + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart(View view) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationEnd(View view) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationCancel(View view) { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java new file mode 100644 index 00000000000..b189d8a8699 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * Implementors of this interface can add themselves as update listeners + * to an ViewPropertyAnimatorCompat instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ViewPropertyAnimatorCompat. + */ +public interface ViewPropertyAnimatorUpdateListener { + + /** + *

    Notifies the occurrence of another frame of the animation.

    + * + * @param view The view associated with the ViewPropertyAnimatorCompat + */ + void onAnimationUpdate(View view); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java new file mode 100644 index 00000000000..15fe7154b74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.Window; + +/** + * Helper for accessing features in {@link Window} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class WindowCompat { + /** + * Flag for enabling the Action Bar. + * This is enabled by default for some devices. The Action Bar + * replaces the title bar and provides an alternate location + * for an on-screen menu button on some devices. + */ + public static final int FEATURE_ACTION_BAR = 8; + + /** + * Flag for requesting an Action Bar that overlays window content. + * Normally an Action Bar will sit in the space above window content, but if this + * feature is requested along with {@link #FEATURE_ACTION_BAR} it will be layered over + * the window content itself. This is useful if you would like your app to have more control + * over how the Action Bar is displayed, such as letting application content scroll beneath + * an Action Bar with a transparent background or otherwise displaying a transparent/translucent + * Action Bar over application content. + * + *

    This mode is especially useful with {@link View#SYSTEM_UI_FLAG_FULLSCREEN + * View.SYSTEM_UI_FLAG_FULLSCREEN}, which allows you to seamlessly hide the + * action bar in conjunction with other screen decorations. + * + *

    As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, when an + * ActionBar is in this mode it will adjust the insets provided to + * {@link View#fitSystemWindows(android.graphics.Rect) View.fitSystemWindows(Rect)} + * to include the content covered by the action bar, so you can do layout within + * that space. + */ + public static final int FEATURE_ACTION_BAR_OVERLAY = 9; + + /** + * Flag for specifying the behavior of action modes when an Action Bar is not present. + * If overlay is enabled, the action mode UI will be allowed to cover existing window content. + */ + public static final int FEATURE_ACTION_MODE_OVERLAY = 10; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java new file mode 100644 index 00000000000..45959a9768e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java @@ -0,0 +1,259 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.graphics.Rect; + +/** + * Describes a set of insets for window content. + * + *

    WindowInsetsCompats are immutable and may be expanded to include more inset types in the + * future. To adjust insets, use one of the supplied clone methods to obtain a new + * WindowInsetsCompat instance with the adjusted properties.

    + */ +public class WindowInsetsCompat { + + /** Private ctor */ + WindowInsetsCompat() {} + + /** + * Returns the left system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The left system window inset + */ + public int getSystemWindowInsetLeft() { + return 0; + } + + /** + * Returns the top system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The top system window inset + */ + public int getSystemWindowInsetTop() { + return 0; + } + + /** + * Returns the right system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The right system window inset + */ + public int getSystemWindowInsetRight() { + return 0; + } + + /** + * Returns the bottom system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The bottom system window inset + */ + public int getSystemWindowInsetBottom() { + return 0; + } + + /** + * Returns true if this WindowInsets has nonzero system window insets. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return true if any of the system window inset values are nonzero + */ + public boolean hasSystemWindowInsets() { + return false; + } + + /** + * Returns true if this WindowInsets has any nonzero insets. + * + * @return true if any inset values are nonzero + */ + public boolean hasInsets() { + return false; + } + + /** + * Check if these insets have been fully consumed. + * + *

    Insets are considered "consumed" if the applicable consume* methods + * have been called such that all insets have been set to zero. This affects propagation of + * insets through the view hierarchy; insets that have not been fully consumed will continue + * to propagate down to child views.

    + * + *

    The result of this method is equivalent to the return value of + * {@link android.view.View#fitSystemWindows(android.graphics.Rect)}.

    + * + * @return true if the insets have been fully consumed. + */ + public boolean isConsumed() { + return false; + } + + /** + * Returns true if the associated window has a round shape. + * + *

    A round window's left, top, right and bottom edges reach all the way to the + * associated edges of the window but the corners may not be visible. Views responding + * to round insets should take care to not lay out critical elements within the corners + * where they may not be accessible.

    + * + * @return True if the window is round + */ + public boolean isRound() { + return false; + } + + /** + * Returns a copy of this WindowInsets with the system window insets fully consumed. + * + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat consumeSystemWindowInsets() { + return this; + } + + /** + * Returns a copy of this WindowInsets with selected system window insets replaced + * with new values. + * + * @param left New left inset in pixels + * @param top New top inset in pixels + * @param right New right inset in pixels + * @param bottom New bottom inset in pixels + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right, int bottom) { + return this; + } + + /** + * Returns a copy of this WindowInsets with selected system window insets replaced + * with new values. + * + * @param systemWindowInsets New system window insets. Each field is the inset in pixels + * for that edge + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat replaceSystemWindowInsets(Rect systemWindowInsets) { + return this; + } + + /** + * Returns the top stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The top stable inset + */ + public int getStableInsetTop() { + return 0; + } + + + /** + * Returns the left stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The left stable inset + */ + public int getStableInsetLeft() { + return 0; + } + + /** + * Returns the right stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The right stable inset + */ + public int getStableInsetRight() { + return 0; + } + + + /** + * Returns the bottom stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The bottom stable inset + */ + public int getStableInsetBottom() { + return 0; + } + + /** + * Returns true if this WindowInsets has nonzero stable insets. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return true if any of the stable inset values are nonzero + */ + public boolean hasStableInsets() { + return false; + } + + /** + * Returns a copy of this WindowInsets with the stable insets fully consumed. + * + * @return A modified copy of this WindowInsetsCompat + */ + public WindowInsetsCompat consumeStableInsets() { + return this; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java new file mode 100644 index 00000000000..c3237a4d49d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java @@ -0,0 +1,118 @@ +/* + * 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. + */ + +package android.support.v4.view; + +import android.graphics.Rect; +import android.view.WindowInsets; + +class WindowInsetsCompatApi21 extends WindowInsetsCompat { + + private final WindowInsets mSource; + + WindowInsetsCompatApi21(WindowInsets source) { + mSource = source; + } + + @Override + public int getSystemWindowInsetLeft() { + return mSource.getSystemWindowInsetLeft(); + } + + @Override + public int getSystemWindowInsetTop() { + return mSource.getSystemWindowInsetTop(); + } + + @Override + public int getSystemWindowInsetRight() { + return mSource.getSystemWindowInsetRight(); + } + + @Override + public int getSystemWindowInsetBottom() { + return mSource.getSystemWindowInsetBottom(); + } + + @Override + public boolean hasSystemWindowInsets() { + return mSource.hasSystemWindowInsets(); + } + + @Override + public boolean hasInsets() { + return mSource.hasInsets(); + } + + @Override + public boolean isConsumed() { + return mSource.isConsumed(); + } + + @Override + public boolean isRound() { + return mSource.isRound(); + } + + @Override + public WindowInsetsCompat consumeSystemWindowInsets() { + return new WindowInsetsCompatApi21(mSource.consumeSystemWindowInsets()); + } + + @Override + public WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right, int bottom) { + return new WindowInsetsCompatApi21(mSource.replaceSystemWindowInsets(left, top, right, bottom)); + } + + @Override + public WindowInsetsCompat replaceSystemWindowInsets(Rect systemWindowInsets) { + return new WindowInsetsCompatApi21(mSource.replaceSystemWindowInsets(systemWindowInsets)); + } + + @Override + public int getStableInsetTop() { + return mSource.getStableInsetTop(); + } + + @Override + public int getStableInsetLeft() { + return mSource.getStableInsetLeft(); + } + + @Override + public int getStableInsetRight() { + return mSource.getStableInsetRight(); + } + + @Override + public int getStableInsetBottom() { + return mSource.getStableInsetBottom(); + } + + @Override + public boolean hasStableInsets() { + return mSource.hasStableInsets(); + } + + @Override + public WindowInsetsCompat consumeStableInsets() { + return new WindowInsetsCompatApi21(mSource.consumeStableInsets()); + } + + WindowInsets unwrap() { + return mSource; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java new file mode 100644 index 00000000000..297d6464dc7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.os.Build; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing features in {@link AccessibilityEvent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityEventCompat { + + static interface AccessibilityEventVersionImpl { + int getRecordCount(AccessibilityEvent event); + void appendRecord(AccessibilityEvent event, Object record); + Object getRecord(AccessibilityEvent event, int index); + void setContentChangeTypes(AccessibilityEvent event, int types); + int getContentChangeTypes(AccessibilityEvent event); + } + + static class AccessibilityEventStubImpl implements AccessibilityEventVersionImpl { + + @Override + public void appendRecord(AccessibilityEvent event, Object record) { + + } + + @Override + public Object getRecord(AccessibilityEvent event, int index) { + return null; + } + + @Override + public void setContentChangeTypes(AccessibilityEvent event, int types) { + + } + + @Override + public int getRecordCount(AccessibilityEvent event) { + return 0; + } + + @Override + public int getContentChangeTypes(AccessibilityEvent event) { + return 0; + } + } + + static class AccessibilityEventIcsImpl extends AccessibilityEventStubImpl { + + @Override + public void appendRecord(AccessibilityEvent event, Object record) { + AccessibilityEventCompatIcs.appendRecord(event, record); + } + + @Override + public Object getRecord(AccessibilityEvent event, int index) { + return AccessibilityEventCompatIcs.getRecord(event, index); + } + + @Override + public int getRecordCount(AccessibilityEvent event) { + return AccessibilityEventCompatIcs.getRecordCount(event); + } + } + + static class AccessibilityEventKitKatImpl extends AccessibilityEventIcsImpl { + + @Override + public void setContentChangeTypes(AccessibilityEvent event, int types) { + AccessibilityEventCompatKitKat.setContentChangeTypes(event, types); + } + + @Override + public int getContentChangeTypes(AccessibilityEvent event) { + return AccessibilityEventCompatKitKat.getContentChangeTypes(event); + } + } + + private final static AccessibilityEventVersionImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityEventKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityEventIcsImpl(); + } else { + IMPL = new AccessibilityEventStubImpl(); + } + } + + /** + * Represents the event of a hover enter over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080; + + /** + * Represents the event of a hover exit over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100; + + /** + * Represents the event of starting a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200; + + /** + * Represents the event of ending a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; + + /** + * Represents the event of changing the content of a window. + */ + public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; + + /** + * Represents the event of scrolling a view. + */ + public static final int TYPE_VIEW_SCROLLED = 0x00001000; + + /** + * Represents the event of changing the selection in an {@link android.widget.EditText}. + */ + public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000; + + /** + * Represents the event of an application making an announcement. + */ + public static final int TYPE_ANNOUNCEMENT = 0x00004000; + + /** + * Represents the event of gaining accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; + + /** + * Represents the event of clearing accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; + + /** + * Represents the event of traversing the text of a view at a given movement granularity. + */ + public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000; + + /** + * Represents the event of beginning gesture detection. + */ + public static final int TYPE_GESTURE_DETECTION_START = 0x00040000; + + /** + * Represents the event of ending gesture detection. + */ + public static final int TYPE_GESTURE_DETECTION_END = 0x00080000; + + /** + * Represents the event of the user starting to touch the screen. + */ + public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000; + + /** + * Represents the event of the user ending to touch the screen. + */ + public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The type of change is not defined. + */ + public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * A node in the subtree rooted at the source node was added or removed. + */ + public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The node's text changed. + */ + public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The node's content description changed. + */ + public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004; + + /** + * Mask for {@link AccessibilityEvent} all types. + * + * @see AccessibilityEvent#TYPE_VIEW_CLICKED + * @see AccessibilityEvent#TYPE_VIEW_LONG_CLICKED + * @see AccessibilityEvent#TYPE_VIEW_SELECTED + * @see AccessibilityEvent#TYPE_VIEW_FOCUSED + * @see AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED + * @see AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED + * @see AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED + * @see #TYPE_VIEW_HOVER_ENTER + * @see #TYPE_VIEW_HOVER_EXIT + * @see #TYPE_TOUCH_EXPLORATION_GESTURE_START + * @see #TYPE_TOUCH_EXPLORATION_GESTURE_END + * @see #TYPE_WINDOW_CONTENT_CHANGED + * @see #TYPE_VIEW_SCROLLED + * @see #TYPE_VIEW_TEXT_SELECTION_CHANGED + * @see #TYPE_ANNOUNCEMENT + * @see #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY + * @see #TYPE_GESTURE_DETECTION_START + * @see #TYPE_GESTURE_DETECTION_END + * @see #TYPE_TOUCH_INTERACTION_START + * @see #TYPE_TOUCH_INTERACTION_END + */ + public static final int TYPES_ALL_MASK = 0xFFFFFFFF; + + /* + * Hide constructor from clients. + */ + private AccessibilityEventCompat() { + + } + + /** + * Gets the number of records contained in the event. + * + * @return The number of records. + */ + public static int getRecordCount(AccessibilityEvent event) { + return IMPL.getRecordCount(event); + } + + /** + * Appends an {@link android.view.accessibility.AccessibilityRecord} to the end of + * event records. + * + * @param record The record to append. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public static void appendRecord(AccessibilityEvent event, AccessibilityRecordCompat record) { + IMPL.appendRecord(event, record.getImpl()); + } + + /** + * Gets the record at a given index. + * + * @param index The index. + * @return The record at the specified index. + */ + public static AccessibilityRecordCompat getRecord(AccessibilityEvent event, int index) { + return new AccessibilityRecordCompat(IMPL.getRecord(event, index)); + } + + /** + * Creates an {@link AccessibilityRecordCompat} from an {@link AccessibilityEvent} + * that can be used to manipulate the event properties defined in + * {@link android.view.accessibility.AccessibilityRecord}. + *

    + * Note: Do not call {@link AccessibilityRecordCompat#recycle()} on the + * returned {@link AccessibilityRecordCompat}. Call {@link AccessibilityEvent#recycle()} + * in case you want to recycle the event. + *

    + * + * @param event The from which to create a record. + * @return An {@link AccessibilityRecordCompat}. + */ + public static AccessibilityRecordCompat asRecord(AccessibilityEvent event) { + return new AccessibilityRecordCompat(event); + } + + /** + * Sets the bit mask of node tree changes signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. + * + * @param changeTypes The bit mask of change types. + * @throws IllegalStateException If called from an AccessibilityService. + * @see #getContentChangeTypes(AccessibilityEvent) + */ + public static void setContentChangeTypes(AccessibilityEvent event, int changeTypes) { + IMPL.setContentChangeTypes(event, changeTypes); + } + + /** + * Gets the bit mask of change types signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent + * multiple change types. + * + * @return The bit mask of change types. One or more of: + *
      + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + *
    + */ + public static int getContentChangeTypes(AccessibilityEvent event) { + return IMPL.getContentChangeTypes(event); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java new file mode 100644 index 00000000000..632c4d12447 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityRecord; + +/** + * ICS specific AccessibilityEvent API implementation. + */ +class AccessibilityEventCompatIcs { + + public static int getRecordCount(AccessibilityEvent event) { + return event.getRecordCount(); + } + + public static void appendRecord(AccessibilityEvent event, Object record) { + event.appendRecord((AccessibilityRecord) record); + } + + public static Object getRecord(AccessibilityEvent event, int index) { + return event.getRecord(index); + } + + public static void setScrollable(AccessibilityEvent event, boolean scrollable) { + event.setScrollable(scrollable); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java new file mode 100644 index 00000000000..1a2cd941dcb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * 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.v4.view.accessibility; + +import android.view.accessibility.AccessibilityEvent; + +class AccessibilityEventCompatKitKat { + public static void setContentChangeTypes(AccessibilityEvent event, int changeTypes) { + event.setContentChangeTypes(changeTypes); + } + + public static int getContentChangeTypes(AccessibilityEvent event) { + return event.getContentChangeTypes(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java new file mode 100644 index 00000000000..ee2da3312c7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.os.Build; +import android.support.v4.view.accessibility.AccessibilityManagerCompatIcs.AccessibilityStateChangeListenerBridge; +import android.view.accessibility.AccessibilityManager; + +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing features in {@link AccessibilityManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityManagerCompat { + + interface AccessibilityManagerVersionImpl { + public Object newAccessiblityStateChangeListener( + AccessibilityStateChangeListenerCompat listener); + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener); + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener); + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager,int feedbackTypeFlags); + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager); + public boolean isTouchExplorationEnabled(AccessibilityManager manager); + } + + static class AccessibilityManagerStubImpl implements AccessibilityManagerVersionImpl { + public Object newAccessiblityStateChangeListener( + AccessibilityStateChangeListenerCompat listener) { + return null; + } + + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return false; + } + + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return false; + } + + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return Collections.emptyList(); + } + + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return Collections.emptyList(); + } + + public boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return false; + } + } + + static class AccessibilityManagerIcsImpl extends AccessibilityManagerStubImpl { + + @Override + public Object newAccessiblityStateChangeListener( + final AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.newAccessibilityStateChangeListener( + new AccessibilityStateChangeListenerBridge() { + public void onAccessibilityStateChanged(boolean enabled) { + listener.onAccessibilityStateChanged(enabled); + } + }); + } + + @Override + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.addAccessibilityStateChangeListener(manager, + listener.mListener); + } + + @Override + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + final AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.removeAccessibilityStateChangeListener(manager, + listener.mListener); + } + + @Override + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return AccessibilityManagerCompatIcs.getEnabledAccessibilityServiceList(manager, + feedbackTypeFlags); + } + + @Override + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return AccessibilityManagerCompatIcs.getInstalledAccessibilityServiceList(manager); + } + + @Override + public boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return AccessibilityManagerCompatIcs.isTouchExplorationEnabled(manager); + } + } + + static { + if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityManagerIcsImpl(); + } else { + IMPL = new AccessibilityManagerStubImpl(); + } + } + + private static final AccessibilityManagerVersionImpl IMPL; + + /** + * Registers an {@link AccessibilityManager.AccessibilityStateChangeListener} for changes in + * the global accessibility state of the system. + * + * @param manager The accessibility manager. + * @param listener The listener. + * @return True if successfully registered. + */ + public static boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return IMPL.addAccessibilityStateChangeListener(manager, listener); + } + + /** + * Unregisters an {@link AccessibilityManager.AccessibilityStateChangeListener}. + * + * @param manager The accessibility manager. + * @param listener The listener. + * @return True if successfully unregistered. + */ + public static boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return IMPL.removeAccessibilityStateChangeListener(manager, listener); + } + + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @param manager The accessibility manager. + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ + public static List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return IMPL.getInstalledAccessibilityServiceList(manager); + } + + /** + * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services + * for a given feedback type. + * + * @param manager The accessibility manager. + * @param feedbackTypeFlags The feedback type flags. + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + * + * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE + * @see AccessibilityServiceInfo#FEEDBACK_GENERIC + * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC + * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN + * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + */ + public static List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return IMPL.getEnabledAccessibilityServiceList(manager, feedbackTypeFlags); + } + + /** + * Returns if the touch exploration in the system is enabled. + * + * @param manager The accessibility manager. + * @return True if touch exploration is enabled, false otherwise. + */ + public static boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return IMPL.isTouchExplorationEnabled(manager); + } + + /** + * Listener for the accessibility state. + */ + public static abstract class AccessibilityStateChangeListenerCompat { + final Object mListener; + + public AccessibilityStateChangeListenerCompat() { + mListener = IMPL.newAccessiblityStateChangeListener(this); + } + + /** + * Called back on change in the accessibility state. + * + * @param enabled Whether accessibility is enabled. + */ + public abstract void onAccessibilityStateChanged(boolean enabled); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java new file mode 100644 index 00000000000..4d4e024af02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; + +import java.util.List; + +/** + * ICS specific AccessibilityManager API implementation. + */ +class AccessibilityManagerCompatIcs { + + interface AccessibilityStateChangeListenerBridge { + public void onAccessibilityStateChanged(boolean enabled); + } + + public static Object newAccessibilityStateChangeListener( + final AccessibilityStateChangeListenerBridge bridge) { + return new AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enabled) { + bridge.onAccessibilityStateChanged(enabled); + } + }; + } + + public static boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + Object listener) { + return manager.addAccessibilityStateChangeListener( + (AccessibilityStateChangeListener)listener); + } + + public static boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + Object listener) { + return manager.removeAccessibilityStateChangeListener( + (AccessibilityStateChangeListener)listener); + } + + public static List getEnabledAccessibilityServiceList( + AccessibilityManager manager,int feedbackTypeFlags) { + return manager.getEnabledAccessibilityServiceList(feedbackTypeFlags); + } + + public static List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return manager.getInstalledAccessibilityServiceList(); + } + + public static boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return manager.isTouchExplorationEnabled(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java new file mode 100644 index 00000000000..1b239a73241 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java @@ -0,0 +1,2787 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.accessibilityservice.AccessibilityServiceInfoCompat; +import android.support.v4.view.ViewCompat; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityNodeInfo} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityNodeInfoCompat { + + public static class AccessibilityActionCompat { + private final Object mAction; + + /** + * Creates a new instance. + * + * @param actionId The action id. + * @param label The action label. + */ + public AccessibilityActionCompat(int actionId, CharSequence label) { + this(IMPL.newAccessibilityAction(actionId, label)); + } + + private AccessibilityActionCompat(Object action) { + mAction = action; + } + + /** + * Gets the id for this action. + * + * @return The action id. + */ + public int getId() { + return IMPL.getAccessibilityActionId(mAction); + } + + /** + * Gets the label for this action. Its purpose is to describe the + * action to user. + * + * @return The label. + */ + public CharSequence getLabel() { + return IMPL.getAccessibilityActionLabel(mAction); + } + } + + public static class CollectionInfoCompat { + /** Selection mode where items are not selectable. */ + public static final int SELECTION_MODE_NONE = 0; + + /** Selection mode where a single item may be selected. */ + public static final int SELECTION_MODE_SINGLE = 1; + + /** Selection mode where multiple items may be selected. */ + public static final int SELECTION_MODE_MULTIPLE = 2; + + final Object mInfo; + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static CollectionInfoCompat obtain(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return new CollectionInfoCompat(IMPL.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode)); + } + + private CollectionInfoCompat(Object info) { + mInfo = info; + } + + public int getColumnCount() { + return IMPL.getCollectionInfoColumnCount(mInfo); + } + + public int getRowCount() { + return IMPL.getCollectionInfoRowCount(mInfo); + } + + public boolean isHierarchical() { + return IMPL.isCollectionInfoHierarchical(mInfo); + } + } + + public static class CollectionItemInfoCompat { + + private final Object mInfo; + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static CollectionItemInfoCompat obtain(int rowIndex, int rowSpan, + int columnIndex, int columnSpan, boolean heading, boolean selected) { + return new CollectionItemInfoCompat(IMPL.obtainCollectionItemInfo(rowIndex, rowSpan, + columnIndex, columnSpan, heading, selected)); + } + + private CollectionItemInfoCompat(Object info) { + mInfo = info; + } + + public int getColumnIndex() { + return IMPL.getCollectionItemColumnIndex(mInfo); + } + + public int getColumnSpan() { + return IMPL.getCollectionItemColumnSpan(mInfo); + } + + public int getRowIndex() { + return IMPL.getCollectionItemRowIndex(mInfo); + } + + public int getRowSpan() { + return IMPL.getCollectionItemRowSpan(mInfo); + } + + public boolean isHeading() { + return IMPL.isCollectionItemHeading(mInfo); + } + + public boolean isSelected() { + return IMPL.isCollectionItemSelected(mInfo); + } + } + + public static class RangeInfoCompat { + /** Range type: integer. */ + public static final int RANGE_TYPE_INT = 0; + /** Range type: float. */ + public static final int RANGE_TYPE_FLOAT = 1; + /** Range type: percent with values from zero to one.*/ + public static final int RANGE_TYPE_PERCENT = 2; + + private final Object mInfo; + + private RangeInfoCompat(Object info) { + mInfo = info; + } + + public float getCurrent() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getCurrent(mInfo); + } + + public float getMax() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMax(mInfo); + } + + public float getMin() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMin(mInfo); + } + + public int getType() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getType(mInfo); + } + } + + static interface AccessibilityNodeInfoImpl { + public Object newAccessibilityAction(int actionId, CharSequence label); + public Object obtain(); + public Object obtain(View source); + public Object obtain(Object info); + public Object obtain(View root, int virtualDescendantId); + public void setSource(Object info, View source); + public void setSource(Object info, View root, int virtualDescendantId); + public Object findFocus(Object info, int focus); + public Object focusSearch(Object info, int direction); + public int getWindowId(Object info); + public int getChildCount(Object info); + public Object getChild(Object info, int index); + public void addChild(Object info, View child); + public void addChild(Object info, View child, int virtualDescendantId); + public int getActions(Object info); + public void addAction(Object info, int action); + public void addAction(Object info, Object action); + public int getAccessibilityActionId(Object action); + public CharSequence getAccessibilityActionLabel(Object action); + public boolean performAction(Object info, int action); + public boolean performAction(Object info, int action, Bundle arguments); + public void setMovementGranularities(Object info, int granularities); + public int getMovementGranularities(Object info); + public List findAccessibilityNodeInfosByText(Object info, String text); + public Object getParent(Object info); + public void setParent(Object info, View root, int virtualDescendantId); + public void setParent(Object info, View parent); + public void getBoundsInParent(Object info, Rect outBounds); + public void setBoundsInParent(Object info, Rect bounds); + public void getBoundsInScreen(Object info, Rect outBounds); + public void setBoundsInScreen(Object info, Rect bounds); + public boolean isCheckable(Object info); + public void setCheckable(Object info, boolean checkable); + public boolean isChecked(Object info); + public void setChecked(Object info, boolean checked); + public boolean isFocusable(Object info); + public void setFocusable(Object info, boolean focusable); + public boolean isFocused(Object info); + public void setFocused(Object info, boolean focused); + public boolean isVisibleToUser(Object info); + public void setVisibleToUser(Object info, boolean visibleToUser); + public boolean isAccessibilityFocused(Object info); + public void setAccessibilityFocused(Object info, boolean focused); + public boolean isSelected(Object info); + public void setSelected(Object info, boolean selected); + public boolean isClickable(Object info); + public void setClickable(Object info, boolean clickable); + public boolean isLongClickable(Object info); + public void setLongClickable(Object info, boolean longClickable); + public boolean isEnabled(Object info); + public void setEnabled(Object info, boolean enabled); + public boolean isPassword(Object info); + public void setPassword(Object info, boolean password); + public boolean isScrollable(Object info); + public void setScrollable(Object info, boolean scrollable); + public CharSequence getPackageName(Object info); + public void setPackageName(Object info, CharSequence packageName); + public CharSequence getClassName(Object info); + public void setClassName(Object info, CharSequence className); + public CharSequence getText(Object info); + public void setText(Object info, CharSequence text); + public CharSequence getContentDescription(Object info); + public void setContentDescription(Object info, CharSequence contentDescription); + public void recycle(Object info); + public String getViewIdResourceName(Object info); + public void setViewIdResourceName(Object info, String viewId); + public int getLiveRegion(Object info); + public void setLiveRegion(Object info, int mode); + public Object getCollectionInfo(Object info); + public void setCollectionInfo(Object info, Object collectionInfo); + public Object getCollectionItemInfo(Object info); + public void setCollectionItemInfo(Object info, Object collectionItemInfo); + public Object getRangeInfo(Object info); + public List getActionList(Object info); + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode); + public int getCollectionInfoColumnCount(Object info); + public int getCollectionInfoRowCount(Object info); + public boolean isCollectionInfoHierarchical(Object info); + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected); + public int getCollectionItemColumnIndex(Object info); + public int getCollectionItemColumnSpan(Object info); + public int getCollectionItemRowIndex(Object info); + public int getCollectionItemRowSpan(Object info); + public boolean isCollectionItemHeading(Object info); + public boolean isCollectionItemSelected(Object info); + public AccessibilityNodeInfoCompat getTraversalBefore(Object info); + public void setTraversalBefore(Object info, View view); + public void setTraversalBefore(Object info, View root, int virtualDescendantId); + public AccessibilityNodeInfoCompat getTraversalAfter(Object info); + public void setTraversalAfter(Object info, View view); + public void setTraversalAfter(Object info, View root, int virtualDescendantId); + public void setContentInvalid(Object info, boolean contentInvalid); + public boolean isContentInvalid(Object info); + public void setError(Object info, CharSequence error); + public CharSequence getError(Object info); + public void setLabelFor(Object info, View labeled); + public void setLabelFor(Object info, View root, int virtualDescendantId); + } + + static class AccessibilityNodeInfoStubImpl implements AccessibilityNodeInfoImpl { + @Override + public Object newAccessibilityAction(int actionId, CharSequence label) { + return null; + } + + @Override + public Object obtain() { + return null; + } + + @Override + public Object obtain(View source) { + return null; + } + + @Override + public Object obtain(View root, int virtualDescendantId) { + return null; + } + + @Override + public Object obtain(Object info) { + return null; + } + + @Override + public void addAction(Object info, int action) { + + } + + @Override + public void addAction(Object info, Object action) { + + } + + @Override + public int getAccessibilityActionId(Object action) { + return 0; + } + + @Override + public CharSequence getAccessibilityActionLabel(Object action) { + return null; + } + + @Override + public void addChild(Object info, View child) { + + } + + @Override + public void addChild(Object info, View child, int virtualDescendantId) { + + } + + @Override + public List findAccessibilityNodeInfosByText(Object info, String text) { + return Collections.emptyList(); + } + + @Override + public int getActions(Object info) { + return 0; + } + + @Override + public void getBoundsInParent(Object info, Rect outBounds) { + + } + + @Override + public void getBoundsInScreen(Object info, Rect outBounds) { + + } + + @Override + public Object getChild(Object info, int index) { + return null; + } + + @Override + public int getChildCount(Object info) { + return 0; + } + + @Override + public CharSequence getClassName(Object info) { + return null; + } + + @Override + public CharSequence getContentDescription(Object info) { + return null; + } + + @Override + public CharSequence getPackageName(Object info) { + return null; + } + + @Override + public Object getParent(Object info) { + return null; + } + + @Override + public CharSequence getText(Object info) { + return null; + } + + @Override + public int getWindowId(Object info) { + return 0; + } + + @Override + public boolean isCheckable(Object info) { + return false; + } + + @Override + public boolean isChecked(Object info) { + return false; + } + + @Override + public boolean isClickable(Object info) { + return false; + } + + @Override + public boolean isEnabled(Object info) { + return false; + } + + @Override + public boolean isFocusable(Object info) { + return false; + } + + @Override + public boolean isFocused(Object info) { + return false; + } + + @Override + public boolean isVisibleToUser(Object info) { + return false; + } + + @Override + public boolean isAccessibilityFocused(Object info) { + return false; + } + + @Override + public boolean isLongClickable(Object info) { + return false; + } + + @Override + public boolean isPassword(Object info) { + return false; + } + + @Override + public boolean isScrollable(Object info) { + return false; + } + + @Override + public boolean isSelected(Object info) { + return false; + } + + @Override + public boolean performAction(Object info, int action) { + return false; + } + + @Override + public boolean performAction(Object info, int action, Bundle arguments) { + return false; + } + + @Override + public void setMovementGranularities(Object info, int granularities) { + + } + + @Override + public int getMovementGranularities(Object info) { + return 0; + } + + @Override + public void setBoundsInParent(Object info, Rect bounds) { + + } + + @Override + public void setBoundsInScreen(Object info, Rect bounds) { + + } + + @Override + public void setCheckable(Object info, boolean checkable) { + + } + + @Override + public void setChecked(Object info, boolean checked) { + + } + + @Override + public void setClassName(Object info, CharSequence className) { + + } + + @Override + public void setClickable(Object info, boolean clickable) { + + } + + @Override + public void setContentDescription(Object info, CharSequence contentDescription) { + + } + + @Override + public void setEnabled(Object info, boolean enabled) { + + } + + @Override + public void setFocusable(Object info, boolean focusable) { + + } + + @Override + public void setFocused(Object info, boolean focused) { + + } + + @Override + public void setVisibleToUser(Object info, boolean visibleToUser) { + + } + + @Override + public void setAccessibilityFocused(Object info, boolean focused) { + + } + + @Override + public void setLongClickable(Object info, boolean longClickable) { + + } + + @Override + public void setPackageName(Object info, CharSequence packageName) { + + } + + @Override + public void setParent(Object info, View parent) { + + } + + @Override + public void setPassword(Object info, boolean password) { + + } + + @Override + public void setScrollable(Object info, boolean scrollable) { + + } + + @Override + public void setSelected(Object info, boolean selected) { + + } + + @Override + public void setSource(Object info, View source) { + + } + + @Override + public void setSource(Object info, View root, int virtualDescendantId) { + + } + + @Override + public Object findFocus(Object info, int focus) { + return null; + } + + @Override + public Object focusSearch(Object info, int direction) { + return null; + } + + @Override + public void setText(Object info, CharSequence text) { + + } + + @Override + public void recycle(Object info) { + + } + + @Override + public void setParent(Object info, View root, int virtualDescendantId) { + + } + + @Override + public String getViewIdResourceName(Object info) { + return null; + } + + @Override + public void setViewIdResourceName(Object info, String viewId) { + + } + + @Override + public int getLiveRegion(Object info) { + return ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE; + } + + @Override + public void setLiveRegion(Object info, int mode) { + // No-op + } + + @Override + public Object getCollectionInfo(Object info) { + return null; + } + + @Override + public void setCollectionInfo(Object info, Object collectionInfo) { + } + + @Override + public Object getCollectionItemInfo(Object info) { + return null; + } + + @Override + public void setCollectionItemInfo(Object info, Object collectionItemInfo) { + } + + @Override + public Object getRangeInfo(Object info) { + return null; + } + + @Override + public List getActionList(Object info) { + return null; + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode) { + return null; + } + + @Override + public int getCollectionInfoColumnCount(Object info) { + return 0; + } + + @Override + public int getCollectionInfoRowCount(Object info) { + return 0; + } + + @Override + public boolean isCollectionInfoHierarchical(Object info) { + return false; + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return null; + } + + @Override + public int getCollectionItemColumnIndex(Object info) { + return 0; + } + + @Override + public int getCollectionItemColumnSpan(Object info) { + return 0; + } + + @Override + public int getCollectionItemRowIndex(Object info) { + return 0; + } + + @Override + public int getCollectionItemRowSpan(Object info) { + return 0; + } + + @Override + public boolean isCollectionItemHeading(Object info) { + return false; + } + + @Override + public boolean isCollectionItemSelected(Object info) { + return false; + } + + @Override + public AccessibilityNodeInfoCompat getTraversalBefore(Object info) { + return null; + } + + @Override + public void setTraversalBefore(Object info, View view) { + } + + @Override + public void setTraversalBefore(Object info, View root, int virtualDescendantId) { + } + + @Override + public AccessibilityNodeInfoCompat getTraversalAfter(Object info) { + return null; + } + + @Override + public void setTraversalAfter(Object info, View view) { + } + + @Override + public void setTraversalAfter(Object info, View root, int virtualDescendantId) { + } + + @Override + public void setContentInvalid(Object info, boolean contentInvalid) { + } + + @Override + public boolean isContentInvalid(Object info) { + return false; + } + + @Override + public void setError(Object info, CharSequence error) { + } + + @Override + public CharSequence getError(Object info) { + return null; + } + + @Override + public void setLabelFor(Object info, View labeled) { + } + + @Override + public void setLabelFor(Object info, View root, int virtualDescendantId) { + } + } + + static class AccessibilityNodeInfoIcsImpl extends AccessibilityNodeInfoStubImpl { + @Override + public Object obtain() { + return AccessibilityNodeInfoCompatIcs.obtain(); + } + + @Override + public Object obtain(View source) { + return AccessibilityNodeInfoCompatIcs.obtain(source); + } + + @Override + public Object obtain(Object info) { + return AccessibilityNodeInfoCompatIcs.obtain(info); + } + + @Override + public void addAction(Object info, int action) { + AccessibilityNodeInfoCompatIcs.addAction(info, action); + } + + @Override + public void addChild(Object info, View child) { + AccessibilityNodeInfoCompatIcs.addChild(info, child); + } + + @Override + public List findAccessibilityNodeInfosByText(Object info, String text) { + return AccessibilityNodeInfoCompatIcs.findAccessibilityNodeInfosByText(info, text); + } + + @Override + public int getActions(Object info) { + return AccessibilityNodeInfoCompatIcs.getActions(info); + } + + @Override + public void getBoundsInParent(Object info, Rect outBounds) { + AccessibilityNodeInfoCompatIcs.getBoundsInParent(info, outBounds); + } + + @Override + public void getBoundsInScreen(Object info, Rect outBounds) { + AccessibilityNodeInfoCompatIcs.getBoundsInScreen(info, outBounds); + } + + @Override + public Object getChild(Object info, int index) { + return AccessibilityNodeInfoCompatIcs.getChild(info, index); + } + + @Override + public int getChildCount(Object info) { + return AccessibilityNodeInfoCompatIcs.getChildCount(info); + } + + @Override + public CharSequence getClassName(Object info) { + return AccessibilityNodeInfoCompatIcs.getClassName(info); + } + + @Override + public CharSequence getContentDescription(Object info) { + return AccessibilityNodeInfoCompatIcs.getContentDescription(info); + } + + @Override + public CharSequence getPackageName(Object info) { + return AccessibilityNodeInfoCompatIcs.getPackageName(info); + } + + @Override + public Object getParent(Object info) { + return AccessibilityNodeInfoCompatIcs.getParent(info); + } + + @Override + public CharSequence getText(Object info) { + return AccessibilityNodeInfoCompatIcs.getText(info); + } + + @Override + public int getWindowId(Object info) { + return AccessibilityNodeInfoCompatIcs.getWindowId(info); + } + + @Override + public boolean isCheckable(Object info) { + return AccessibilityNodeInfoCompatIcs.isCheckable(info); + } + + @Override + public boolean isChecked(Object info) { + return AccessibilityNodeInfoCompatIcs.isChecked(info); + } + + @Override + public boolean isClickable(Object info) { + return AccessibilityNodeInfoCompatIcs.isClickable(info); + } + + @Override + public boolean isEnabled(Object info) { + return AccessibilityNodeInfoCompatIcs.isEnabled(info); + } + + @Override + public boolean isFocusable(Object info) { + return AccessibilityNodeInfoCompatIcs.isFocusable(info); + } + + @Override + public boolean isFocused(Object info) { + return AccessibilityNodeInfoCompatIcs.isFocused(info); + } + + @Override + public boolean isLongClickable(Object info) { + return AccessibilityNodeInfoCompatIcs.isLongClickable(info); + } + + @Override + public boolean isPassword(Object info) { + return AccessibilityNodeInfoCompatIcs.isPassword(info); + } + + @Override + public boolean isScrollable(Object info) { + return AccessibilityNodeInfoCompatIcs.isScrollable(info); + } + + @Override + public boolean isSelected(Object info) { + return AccessibilityNodeInfoCompatIcs.isSelected(info); + } + + @Override + public boolean performAction(Object info, int action) { + return AccessibilityNodeInfoCompatIcs.performAction(info, action); + } + + @Override + public void setBoundsInParent(Object info, Rect bounds) { + AccessibilityNodeInfoCompatIcs.setBoundsInParent(info, bounds); + } + + @Override + public void setBoundsInScreen(Object info, Rect bounds) { + AccessibilityNodeInfoCompatIcs.setBoundsInScreen(info, bounds); + } + + @Override + public void setCheckable(Object info, boolean checkable) { + AccessibilityNodeInfoCompatIcs.setCheckable(info, checkable); + } + + @Override + public void setChecked(Object info, boolean checked) { + AccessibilityNodeInfoCompatIcs.setChecked(info, checked); + } + + @Override + public void setClassName(Object info, CharSequence className) { + AccessibilityNodeInfoCompatIcs.setClassName(info, className); + } + + @Override + public void setClickable(Object info, boolean clickable) { + AccessibilityNodeInfoCompatIcs.setClickable(info, clickable); + } + + @Override + public void setContentDescription(Object info, CharSequence contentDescription) { + AccessibilityNodeInfoCompatIcs.setContentDescription(info, contentDescription); + } + + @Override + public void setEnabled(Object info, boolean enabled) { + AccessibilityNodeInfoCompatIcs.setEnabled(info, enabled); + } + + @Override + public void setFocusable(Object info, boolean focusable) { + AccessibilityNodeInfoCompatIcs.setFocusable(info, focusable); + } + + @Override + public void setFocused(Object info, boolean focused) { + AccessibilityNodeInfoCompatIcs.setFocused(info, focused); + } + + @Override + public void setLongClickable(Object info, boolean longClickable) { + AccessibilityNodeInfoCompatIcs.setLongClickable(info, longClickable); + } + + @Override + public void setPackageName(Object info, CharSequence packageName) { + AccessibilityNodeInfoCompatIcs.setPackageName(info, packageName); + } + + @Override + public void setParent(Object info, View parent) { + AccessibilityNodeInfoCompatIcs.setParent(info, parent); + } + + @Override + public void setPassword(Object info, boolean password) { + AccessibilityNodeInfoCompatIcs.setPassword(info, password); + } + + @Override + public void setScrollable(Object info, boolean scrollable) { + AccessibilityNodeInfoCompatIcs.setScrollable(info, scrollable); + } + + @Override + public void setSelected(Object info, boolean selected) { + AccessibilityNodeInfoCompatIcs.setSelected(info, selected); + } + + @Override + public void setSource(Object info, View source) { + AccessibilityNodeInfoCompatIcs.setSource(info, source); + } + + @Override + public void setText(Object info, CharSequence text) { + AccessibilityNodeInfoCompatIcs.setText(info, text); + } + + @Override + public void recycle(Object info) { + AccessibilityNodeInfoCompatIcs.recycle(info); + } + } + + static class AccessibilityNodeInfoJellybeanImpl extends AccessibilityNodeInfoIcsImpl { + @Override + public Object obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfoCompatJellyBean.obtain(root, virtualDescendantId); + } + + @Override + public Object findFocus(Object info, int focus) { + return AccessibilityNodeInfoCompatJellyBean.findFocus(info, focus); + } + + @Override + public Object focusSearch(Object info, int direction) { + return AccessibilityNodeInfoCompatJellyBean.focusSearch(info, direction); + } + + @Override + public void addChild(Object info, View child, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.addChild(info, child, virtualDescendantId); + } + + @Override + public void setSource(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.setSource(info, root, virtualDescendantId); + } + + @Override + public boolean isVisibleToUser(Object info) { + return AccessibilityNodeInfoCompatJellyBean.isVisibleToUser(info); + } + + @Override + public void setVisibleToUser(Object info, boolean visibleToUser) { + AccessibilityNodeInfoCompatJellyBean.setVisibleToUser(info, visibleToUser); + } + + @Override + public boolean isAccessibilityFocused(Object info) { + return AccessibilityNodeInfoCompatJellyBean.isAccessibilityFocused(info); + } + + @Override + public void setAccessibilityFocused(Object info, boolean focused) { + AccessibilityNodeInfoCompatJellyBean.setAccesibilityFocused(info, focused); + } + + @Override + public boolean performAction(Object info, int action, Bundle arguments) { + return AccessibilityNodeInfoCompatJellyBean.performAction(info, action, arguments); + } + + @Override + public void setMovementGranularities(Object info, int granularities) { + AccessibilityNodeInfoCompatJellyBean.setMovementGranularities(info, granularities); + } + + @Override + public int getMovementGranularities(Object info) { + return AccessibilityNodeInfoCompatJellyBean.getMovementGranularities(info); + } + + @Override + public void setParent(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.setParent(info, root, virtualDescendantId); + } + } + + static class AccessibilityNodeInfoJellybeanMr2Impl extends AccessibilityNodeInfoJellybeanImpl { + + @Override + public String getViewIdResourceName(Object info) { + return AccessibilityNodeInfoCompatJellybeanMr2.getViewIdResourceName(info); + } + + @Override + public void setViewIdResourceName(Object info, String viewId) { + AccessibilityNodeInfoCompatJellybeanMr2.setViewIdResourceName(info, viewId); + } + } + + static class AccessibilityNodeInfoKitKatImpl extends AccessibilityNodeInfoJellybeanMr2Impl { + @Override + public int getLiveRegion(Object info) { + return AccessibilityNodeInfoCompatKitKat.getLiveRegion(info); + } + + @Override + public void setLiveRegion(Object info, int mode) { + AccessibilityNodeInfoCompatKitKat.setLiveRegion(info, mode); + } + + @Override + public Object getCollectionInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getCollectionInfo(info); + } + + @Override + public void setCollectionInfo(Object info, Object collectionInfo) { + AccessibilityNodeInfoCompatKitKat.setCollectionInfo(info, collectionInfo); + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfoCompatKitKat.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode); + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfoCompatKitKat + .obtainCollectionItemInfo(rowIndex, rowSpan, columnIndex, columnSpan, heading); + } + + @Override + public int getCollectionInfoColumnCount(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getColumnCount(info); + } + + @Override + public int getCollectionInfoRowCount(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getRowCount(info); + } + + @Override + public boolean isCollectionInfoHierarchical(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.isHierarchical(info); + } + + @Override + public Object getCollectionItemInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getCollectionItemInfo(info); + } + + @Override + public Object getRangeInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getRangeInfo(info); + } + + @Override + public int getCollectionItemColumnIndex(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnIndex(info); + } + + @Override + public int getCollectionItemColumnSpan(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnSpan(info); + } + + @Override + public int getCollectionItemRowIndex(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowIndex(info); + } + + @Override + public int getCollectionItemRowSpan(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowSpan(info); + } + + @Override + public boolean isCollectionItemHeading(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.isHeading(info); + } + + @Override + public void setCollectionItemInfo(Object info, Object collectionItemInfo) { + AccessibilityNodeInfoCompatKitKat.setCollectionItemInfo(info, collectionItemInfo); + } + + @Override + public void setContentInvalid(Object info, boolean contentInvalid) { + AccessibilityNodeInfoCompatKitKat.setContentInvalid(info, contentInvalid); + } + + @Override + public boolean isContentInvalid(Object info) { + return AccessibilityNodeInfoCompatKitKat.isContentInvalid(info); + } + } + + static class AccessibilityNodeInfoApi21Impl extends AccessibilityNodeInfoKitKatImpl { + @Override + public Object newAccessibilityAction(int actionId, CharSequence label) { + return AccessibilityNodeInfoCompatApi21.newAccessibilityAction(actionId, label); + } + + @Override + public List getActionList(Object info) { + return AccessibilityNodeInfoCompatApi21.getActionList(info); + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode) { + return AccessibilityNodeInfoCompatApi21.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode); + } + + @Override + public void addAction(Object info, Object action) { + AccessibilityNodeInfoCompatApi21.addAction(info, action); + } + + @Override + public int getAccessibilityActionId(Object action) { + return AccessibilityNodeInfoCompatApi21.getAccessibilityActionId(action); + } + + @Override + public CharSequence getAccessibilityActionLabel(Object action) { + return AccessibilityNodeInfoCompatApi21.getAccessibilityActionLabel(action); + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfoCompatApi21.obtainCollectionItemInfo(rowIndex, rowSpan, + columnIndex, columnSpan, heading, selected); + } + + @Override + public boolean isCollectionItemSelected(Object info) { + return AccessibilityNodeInfoCompatApi21.CollectionItemInfo.isSelected(info); + } + + @Override + public CharSequence getError(Object info) { + return AccessibilityNodeInfoCompatApi21.getError(info); + } + + @Override + public void setError(Object info, CharSequence error) { + AccessibilityNodeInfoCompatApi21.setError(info, error); + } + + @Override + public void setLabelFor(Object info, View labeled) { + AccessibilityNodeInfoCompatApi21.setLabelFor(info, labeled); + } + + @Override + public void setLabelFor(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi21.setLabelFor(info, root, virtualDescendantId); + } + } + + static class AccessibilityNodeInfoApi22Impl extends AccessibilityNodeInfoApi21Impl { + @Override + public AccessibilityNodeInfoCompat getTraversalBefore(Object info) { + Object nodeInfo = AccessibilityNodeInfoCompatApi22.getTraversalBefore(info); + if (nodeInfo == null) { + return null; + } + + return new AccessibilityNodeInfoCompat(nodeInfo); + } + + @Override + public void setTraversalBefore(Object info, View view) { + AccessibilityNodeInfoCompatApi22.setTraversalBefore(info, view); + } + + @Override + public void setTraversalBefore(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi22.setTraversalBefore(info, root, virtualDescendantId); + } + + @Override + public AccessibilityNodeInfoCompat getTraversalAfter(Object info) { + Object nodeInfo = AccessibilityNodeInfoCompatApi22.getTraversalAfter(info); + if (nodeInfo == null) { + return null; + } + + return new AccessibilityNodeInfoCompat(nodeInfo); + } + + @Override + public void setTraversalAfter(Object info, View view) { + AccessibilityNodeInfoCompatApi22.setTraversalAfter(info, view); + } + + @Override + public void setTraversalAfter(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi22.setTraversalAfter(info, root, virtualDescendantId); + } + } + + static { + if (Build.VERSION.SDK_INT >= 22) { + IMPL = new AccessibilityNodeInfoApi22Impl(); + } else if (Build.VERSION.SDK_INT >= 21) { + IMPL = new AccessibilityNodeInfoApi21Impl(); + } else if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityNodeInfoKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + IMPL = new AccessibilityNodeInfoJellybeanMr2Impl(); + } else if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityNodeInfoJellybeanImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityNodeInfoIcsImpl(); + } else { + IMPL = new AccessibilityNodeInfoStubImpl(); + } + } + + private static final AccessibilityNodeInfoImpl IMPL; + + private final Object mInfo; + + // Actions introduced in IceCreamSandwich + + /** + * Action that focuses the node. + */ + public static final int ACTION_FOCUS = 0x00000001; + + /** + * Action that unfocuses the node. + */ + public static final int ACTION_CLEAR_FOCUS = 0x00000002; + + /** + * Action that selects the node. + */ + public static final int ACTION_SELECT = 0x00000004; + + /** + * Action that unselects the node. + */ + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + /** + * Action that clicks on the node info. + */ + public static final int ACTION_CLICK = 0x00000010; + + /** + * Action that long clicks on the node. + */ + public static final int ACTION_LONG_CLICK = 0x00000020; + + // Actions introduced in JellyBean + + /** + * Action that gives accessibility focus to the node. + */ + public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000040; + + /** + * Action that clears accessibility focus of the node. + */ + public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080; + + /** + * Action that requests to go to the next entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
    + * Example: Move to the previous character and do not extend selection. + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see #setMovementGranularities(int) + * @see #getMovementGranularities() + * + * @see #MOVEMENT_GRANULARITY_CHARACTER + * @see #MOVEMENT_GRANULARITY_WORD + * @see #MOVEMENT_GRANULARITY_LINE + * @see #MOVEMENT_GRANULARITY_PARAGRAPH + * @see #MOVEMENT_GRANULARITY_PAGE + */ + public static final int ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100; + + /** + * Action that requests to go to the previous entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
    + * Example: Move to the next character and do not extend selection. + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, + * arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see #setMovementGranularities(int) + * @see #getMovementGranularities() + * + * @see #MOVEMENT_GRANULARITY_CHARACTER + * @see #MOVEMENT_GRANULARITY_WORD + * @see #MOVEMENT_GRANULARITY_LINE + * @see #MOVEMENT_GRANULARITY_PARAGRAPH + * @see #MOVEMENT_GRANULARITY_PAGE + */ + public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200; + + /** + * Action to move to the next HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments); + *

    + *

    + */ + public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400; + + /** + * Action to move to the previous HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, arguments); + *

    + *

    + */ + public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800; + + /** + * Action to scroll the node content forward. + */ + public static final int ACTION_SCROLL_FORWARD = 0x00001000; + + /** + * Action to scroll the node content backward. + */ + public static final int ACTION_SCROLL_BACKWARD = 0x00002000; + + // Actions introduced in JellyBeanMr2 + + /** + * Action to copy the current selection to the clipboard. + */ + public static final int ACTION_COPY = 0x00004000; + + /** + * Action to paste the current clipboard content. + */ + public static final int ACTION_PASTE = 0x00008000; + + /** + * Action to cut the current selection and place it to the clipboard. + */ + public static final int ACTION_CUT = 0x00010000; + + /** + * Action to set the selection. Performing this action with no arguments + * clears the selection. + *

    + * Arguments: {@link #ACTION_ARGUMENT_SELECTION_START_INT}, + * {@link #ACTION_ARGUMENT_SELECTION_END_INT}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_SELECTION_START_INT + * @see #ACTION_ARGUMENT_SELECTION_END_INT + */ + public static final int ACTION_SET_SELECTION = 0x00020000; + + /** + * Action that sets the text of the node. Performing the action without argument, using + * null or empty {@link CharSequence} will clear the text. This action will also put the + * cursor at the end of text. + *

    + * Arguments: {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + * "android"); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); + *

    + */ + public static final int ACTION_SET_TEXT = 0x00200000; + + // Action arguments + + /** + * Argument for which movement granularity to be used when traversing the node text. + *

    + * Type: int
    + * Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, + * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} + *

    + */ + public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = + "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; + + /** + * Argument for which HTML element to get moving to the next/previous HTML element. + *

    + * Type: String
    + * Actions: {@link #ACTION_NEXT_HTML_ELEMENT}, + * {@link #ACTION_PREVIOUS_HTML_ELEMENT} + *

    + */ + public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = + "ACTION_ARGUMENT_HTML_ELEMENT_STRING"; + + /** + * Argument for whether when moving at granularity to extend the selection + * or to move it otherwise. + *

    + * Type: boolean
    + * Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, + * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} + *

    + * + * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY + * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + */ + public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = + "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"; + + /** + * Argument for specifying the selection start. + *

    + * Type: int
    + * Actions: {@link #ACTION_SET_SELECTION} + *

    + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_START_INT = + "ACTION_ARGUMENT_SELECTION_START_INT"; + + /** + * Argument for specifying the selection end. + *

    + * Type: int
    + * Actions: {@link #ACTION_SET_SELECTION} + *

    + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_END_INT = + "ACTION_ARGUMENT_SELECTION_END_INT"; + + /** + * Argument for specifying the text content to set + *

    + * Type: CharSequence
    + * Actions: {@link #ACTION_SET_TEXT} + *

    + * + * @see #ACTION_SET_TEXT + */ + public static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = + "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"; + + // Focus types + + /** + * The input focus. + */ + public static final int FOCUS_INPUT = 1; + + /** + * The accessibility focus. + */ + public static final int FOCUS_ACCESSIBILITY = 2; + + // Movement granularities + + /** + * Movement granularity bit for traversing the text of a node by character. + */ + public static final int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001; + + /** + * Movement granularity bit for traversing the text of a node by word. + */ + public static final int MOVEMENT_GRANULARITY_WORD = 0x00000002; + + /** + * Movement granularity bit for traversing the text of a node by line. + */ + public static final int MOVEMENT_GRANULARITY_LINE = 0x00000004; + + /** + * Movement granularity bit for traversing the text of a node by paragraph. + */ + public static final int MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008; + + /** + * Movement granularity bit for traversing the text of a node by page. + */ + public static final int MOVEMENT_GRANULARITY_PAGE = 0x00000010; + + /** + * Creates a wrapper for info implementation. + * + * @param object The info to wrap. + * @return A wrapper for if the object is not null, null otherwise. + */ + static AccessibilityNodeInfoCompat wrapNonNullInstance(Object object) { + if (object != null) { + return new AccessibilityNodeInfoCompat(object); + } + return null; + } + + /** + * Creates a new instance wrapping an + * {@link android.view.accessibility.AccessibilityNodeInfo}. + * + * @param info The info. + */ + public AccessibilityNodeInfoCompat(Object info) { + mInfo = info; + } + + /** + * @return The wrapped {@link android.view.accessibility.AccessibilityNodeInfo}. + */ + public Object getInfo() { + return mInfo; + } + + /** + * Returns a cached instance if such is available otherwise a new one and + * sets the source. + * + * @return An instance. + * @see #setSource(View) + */ + public static AccessibilityNodeInfoCompat obtain(View source) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain(source)); + } + + /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + * @return An instance. + * + * @see #setSource(View, int) + */ + public static AccessibilityNodeInfoCompat obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance( + IMPL.obtain(root, virtualDescendantId)); + } + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static AccessibilityNodeInfoCompat obtain() { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain()); + } + + /** + * Returns a cached instance if such is available or a new one is create. + * The returned instance is initialized from the given info. + * + * @param info The other info. + * @return An instance. + */ + public static AccessibilityNodeInfoCompat obtain(AccessibilityNodeInfoCompat info) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain(info.mInfo)); + } + + /** + * Sets the source. + * + * @param source The info source. + */ + public void setSource(View source) { + IMPL.setSource(mInfo, source); + } + + /** + * Sets the source to be a virtual descendant of the given root. + * If virtualDescendantId is {@link View#NO_ID} the root + * is set as the source. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { + IMPL.setSource(mInfo, root, virtualDescendantId); + } + + /** + * Find the view that has the specified focus type. The search starts from + * the view represented by this node info. + * + * @param focus The focus to find. One of {@link #FOCUS_INPUT} or + * {@link #FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see #FOCUS_INPUT + * @see #FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfoCompat findFocus(int focus) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.findFocus(mInfo, focus)); + } + + /** + * Searches for the nearest view in the specified direction that can take + * input focus. + * + * @param direction The direction. Can be one of: + * {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_UP}, + * {@link View#FOCUS_LEFT}, + * {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD}. + * + * @return The node info for the view that can take accessibility focus. + */ + public AccessibilityNodeInfoCompat focusSearch(int direction) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.focusSearch(mInfo, direction)); + } + + /** + * Gets the id of the window from which the info comes from. + * + * @return The window id. + */ + public int getWindowId() { + return IMPL.getWindowId(mInfo); + } + + /** + * Gets the number of children. + * + * @return The child count. + */ + public int getChildCount() { + return IMPL.getChildCount(mInfo); + } + + /** + * Get the child at given index. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link AccessibilityNodeInfoCompat#recycle()} to + * avoid creating of multiple instances. + *

    + * + * @param index The child index. + * @return The child node. + * @throws IllegalStateException If called outside of an + * AccessibilityService. + */ + public AccessibilityNodeInfoCompat getChild(int index) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.getChild(mInfo, index)); + } + + /** + * Adds a child. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param child The child. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addChild(View child) { + IMPL.addChild(mInfo, child); + } + + /** + * Adds a virtual child which is a descendant of the given root. + * If virtualDescendantId is {@link View#NO_ID} the root + * is added as a child. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + */ + public void addChild(View root, int virtualDescendantId) { + IMPL.addChild(mInfo, root, virtualDescendantId); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return The bit mask of with actions. + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_FOCUS + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_FOCUS + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_SELECT + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_SELECTION + */ + public int getActions() { + return IMPL.getActions(mInfo); + } + + /** + * Adds an action that can be performed on the node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param action The action. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(int action) { + IMPL.addAction(mInfo, action); + } + + /** + * Adds an action that can be performed on the node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param action The action. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(AccessibilityActionCompat action) { + IMPL.addAction(mInfo, action.mAction); + } + + /** + * Performs an action on the node. + *

    + * Note: An action can be performed only if the request is + * made from an {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @param action The action to perform. + * @return True if the action was performed. + * @throws IllegalStateException If called outside of an + * AccessibilityService. + */ + public boolean performAction(int action) { + return IMPL.performAction(mInfo, action); + } + + /** + * Performs an action on the node. + *

    + * Note: An action can be performed only if the request is made + * from an {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @param action The action to perform. + * @param arguments A bundle with additional arguments. + * @return True if the action was performed. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + */ + public boolean performAction(int action, Bundle arguments) { + return IMPL.performAction(mInfo, action, arguments); + } + + /** + * Sets the movement granularities for traversing the text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param granularities The bit mask with granularities. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setMovementGranularities(int granularities) { + IMPL.setMovementGranularities(mInfo, granularities); + } + + /** + * Gets the movement granularities for traversing the text of this node. + * + * @return The bit mask with granularities. + */ + public int getMovementGranularities() { + return IMPL.getMovementGranularities(mInfo); + } + + /** + * Finds {@link android.view.accessibility.AccessibilityNodeInfo}s by text. The match + * is case insensitive containment. The search is relative to this info i.e. this + * info is the root of the traversed tree. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link android.view.accessibility.AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + *

    + * + * @param text The searched text. + * @return A list of node info. + */ + public List findAccessibilityNodeInfosByText(String text) { + List result = new ArrayList(); + List infos = IMPL.findAccessibilityNodeInfosByText(mInfo, text); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + Object info = infos.get(i); + result.add(new AccessibilityNodeInfoCompat(info)); + } + return result; + } + + /** + * Gets the parent. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link android.view.accessibility.AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + *

    + * + * @return The parent. + */ + public AccessibilityNodeInfoCompat getParent() { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.getParent(mInfo)); + } + + /** + * Sets the parent. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param parent The parent. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParent(View parent) { + IMPL.setParent(mInfo, parent); + } + + /** + * Sets the parent to be a virtual descendant of the given root. + * If virtualDescendantId equals to {@link View#NO_ID} the root + * is set as the parent. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setParent(View root, int virtualDescendantId) { + IMPL.setParent(mInfo, root, virtualDescendantId); + } + + /** + * Gets the node bounds in parent coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBoundsInParent(Rect outBounds) { + IMPL.getBoundsInParent(mInfo, outBounds); + } + + /** + * Sets the node bounds in parent coordinates. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param bounds The node bounds. + *@throws IllegalStateException If called from an AccessibilityService. + */ + public void setBoundsInParent(Rect bounds) { + IMPL.setBoundsInParent(mInfo, bounds); + } + + /** + * Gets the node bounds in screen coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBoundsInScreen(Rect outBounds) { + IMPL.getBoundsInScreen(mInfo, outBounds); + } + + /** + * Sets the node bounds in screen coordinates. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param bounds The node bounds. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBoundsInScreen(Rect bounds) { + IMPL.setBoundsInScreen(mInfo, bounds); + } + + /** + * Gets whether this node is checkable. + * + * @return True if the node is checkable. + */ + public boolean isCheckable() { + return IMPL.isCheckable(mInfo); + } + + /** + * Sets whether this node is checkable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param checkable True if the node is checkable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCheckable(boolean checkable) { + IMPL.setCheckable(mInfo, checkable); + } + + /** + * Gets whether this node is checked. + * + * @return True if the node is checked. + */ + public boolean isChecked() { + return IMPL.isChecked(mInfo); + } + + /** + * Sets whether this node is checked. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param checked True if the node is checked. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean checked) { + IMPL.setChecked(mInfo, checked); + } + + /** + * Gets whether this node is focusable. + * + * @return True if the node is focusable. + */ + public boolean isFocusable() { + return IMPL.isFocusable(mInfo); + } + + /** + * Sets whether this node is focusable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focusable True if the node is focusable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocusable(boolean focusable) { + IMPL.setFocusable(mInfo, focusable); + } + + /** + * Gets whether this node is focused. + * + * @return True if the node is focused. + */ + public boolean isFocused() { + return IMPL.isFocused(mInfo); + } + + /** + * Sets whether this node is focused. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focused True if the node is focused. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocused(boolean focused) { + IMPL.setFocused(mInfo, focused); + } + + /** + * Sets whether this node is visible to the user. + * + * @return Whether the node is visible to the user. + */ + public boolean isVisibleToUser() { + return IMPL.isVisibleToUser(mInfo); + } + + /** + * Sets whether this node is visible to the user. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param visibleToUser Whether the node is visible to the user. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setVisibleToUser(boolean visibleToUser) { + IMPL.setVisibleToUser(mInfo, visibleToUser); + } + + /** + * Gets whether this node is accessibility focused. + * + * @return True if the node is accessibility focused. + */ + public boolean isAccessibilityFocused() { + return IMPL.isAccessibilityFocused(mInfo); + } + + /** + * Sets whether this node is accessibility focused. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focused True if the node is accessibility focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAccessibilityFocused(boolean focused) { + IMPL.setAccessibilityFocused(mInfo, focused); + } + + /** + * Gets whether this node is selected. + * + * @return True if the node is selected. + */ + public boolean isSelected() { + return IMPL.isSelected(mInfo); + } + + /** + * Sets whether this node is selected. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param selected True if the node is selected. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSelected(boolean selected) { + IMPL.setSelected(mInfo, selected); + } + + /** + * Gets whether this node is clickable. + * + * @return True if the node is clickable. + */ + public boolean isClickable() { + return IMPL.isClickable(mInfo); + } + + /** + * Sets whether this node is clickable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param clickable True if the node is clickable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClickable(boolean clickable) { + IMPL.setClickable(mInfo, clickable); + } + + /** + * Gets whether this node is long clickable. + * + * @return True if the node is long clickable. + */ + public boolean isLongClickable() { + return IMPL.isLongClickable(mInfo); + } + + /** + * Sets whether this node is long clickable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param longClickable True if the node is long clickable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setLongClickable(boolean longClickable) { + IMPL.setLongClickable(mInfo, longClickable); + } + + /** + * Gets whether this node is enabled. + * + * @return True if the node is enabled. + */ + public boolean isEnabled() { + return IMPL.isEnabled(mInfo); + } + + /** + * Sets whether this node is enabled. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param enabled True if the node is enabled. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean enabled) { + IMPL.setEnabled(mInfo, enabled); + } + + /** + * Gets whether this node is a password. + * + * @return True if the node is a password. + */ + public boolean isPassword() { + return IMPL.isPassword(mInfo); + } + + /** + * Sets whether this node is a password. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param password True if the node is a password. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean password) { + IMPL.setPassword(mInfo, password); + } + + /** + * Gets if the node is scrollable. + * + * @return True if the node is scrollable, false otherwise. + */ + public boolean isScrollable() { + return IMPL.isScrollable(mInfo); + } + + /** + * Sets if the node is scrollable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param scrollable True if the node is scrollable, false otherwise. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setScrollable(boolean scrollable) { + IMPL.setScrollable(mInfo, scrollable); + } + + /** + * Gets the package this node comes from. + * + * @return The package name. + */ + public CharSequence getPackageName() { + return IMPL.getPackageName(mInfo); + } + + /** + * Sets the package this node comes from. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param packageName The package name. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPackageName(CharSequence packageName) { + IMPL.setPackageName(mInfo, packageName); + } + + /** + * Gets the class this node comes from. + * + * @return The class name. + */ + public CharSequence getClassName() { + return IMPL.getClassName(mInfo); + } + + /** + * Sets the class this node comes from. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param className The class name. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + IMPL.setClassName(mInfo, className); + } + + /** + * Gets the text of this node. + * + * @return The text. + */ + public CharSequence getText() { + return IMPL.getText(mInfo); + } + + /** + * Sets the text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param text The text. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setText(CharSequence text) { + IMPL.setText(mInfo, text); + } + + /** + * Gets the content description of this node. + * + * @return The content description. + */ + public CharSequence getContentDescription() { + return IMPL.getContentDescription(mInfo); + } + + /** + * Sets the content description of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param contentDescription The content description. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + IMPL.setContentDescription(mInfo, contentDescription); + } + + /** + * Return an instance back to be reused. + *

    + * Note: You must not touch the object after calling this function. + * + * @throws IllegalStateException If the info is already recycled. + */ + public void recycle() { + IMPL.recycle(mInfo); + } + + /** + * Sets the fully qualified resource name of the source view's id. + * + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param viewId The id resource name. + */ + public void setViewIdResourceName(String viewId) { + IMPL.setViewIdResourceName(mInfo, viewId); + } + + /** + * Gets the fully qualified resource name of the source view's id. + * + *

    + * Note: The primary usage of this API is for UI test automation + * and in order to report the source view id of an {@link AccessibilityNodeInfoCompat} + * the client has to set the {@link AccessibilityServiceInfoCompat#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @return The id resource name. + */ + public String getViewIdResourceName() { + return IMPL.getViewIdResourceName(mInfo); + } + + /** + * Gets the node's live region mode. + *

    + * A live region is a node that contains information that is important for + * the user and when it changes the user should be notified. For example, + * in a login screen with a TextView that displays an "incorrect password" + * notification, that view should be marked as a live region with mode + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * It is the responsibility of the accessibility service to monitor + * {@link AccessibilityEventCompat#TYPE_WINDOW_CONTENT_CHANGED} events + * indicating changes to live region nodes and their children. + * + * @return The live region mode, or + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_NONE} if the view is + * not a live region. + * @see ViewCompat#getAccessibilityLiveRegion(View) + */ + public int getLiveRegion() { + return IMPL.getLiveRegion(mInfo); + } + + /** + * Sets the node's live region mode. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + * + * @param mode The live region mode, or + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_NONE} if the view is + * not a live region. + * @see ViewCompat#setAccessibilityLiveRegion(View, int) + */ + public void setLiveRegion(int mode) { + IMPL.setLiveRegion(mInfo, mode); + } + + /** + * Gets the collection info if the node is a collection. A collection + * child is always a collection item. + * + * @return The collection info. + */ + public CollectionInfoCompat getCollectionInfo() { + Object info = IMPL.getCollectionInfo(mInfo); + if (info == null) return null; + return new CollectionInfoCompat(info); + } + + public void setCollectionInfo(Object collectionInfo) { + IMPL.setCollectionInfo(mInfo, ((CollectionInfoCompat) collectionInfo).mInfo); + } + + public void setCollectionItemInfo(Object collectionItemInfo) { + IMPL.setCollectionItemInfo(mInfo, ((CollectionItemInfoCompat) collectionItemInfo).mInfo); + } + + /** + * Gets the collection item info if the node is a collection item. A collection + * item is always a child of a collection. + * + * @return The collection item info. + */ + public CollectionItemInfoCompat getCollectionItemInfo() { + Object info = IMPL.getCollectionItemInfo(mInfo); + if (info == null) return null; + return new CollectionItemInfoCompat(info); + } + + /** + * Gets the range info if this node is a range. + * + * @return The range. + */ + public RangeInfoCompat getRangeInfo() { + Object info = IMPL.getRangeInfo(mInfo); + if (info == null) return null; + return new RangeInfoCompat(info); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return A list of AccessibilityActions. + */ + public List getActionList() { + List actions = IMPL.getActionList(mInfo); + if (actions != null) { + List result = new ArrayList(); + final int actionCount = actions.size(); + for (int i = 0; i < actionCount; i++) { + Object action = actions.get(i); + result.add(new AccessibilityActionCompat(action)); + } + return result; + } else { + return Collections.emptyList(); + } + } + + /** + * Sets if the content of this node is invalid. For example, + * a date is not well-formed. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param contentInvalid If the node content is invalid. + */ + public void setContentInvalid(boolean contentInvalid) { + IMPL.setContentInvalid(mInfo, contentInvalid); + } + + /** + * Gets if the content of this node is invalid. For example, + * a date is not well-formed. + * + * @return If the node content is invalid. + */ + public boolean isContentInvalid() { + return IMPL.isContentInvalid(mInfo); + } + + /** + * Sets the error text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param error The error text. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setError(CharSequence error) { + IMPL.setError(mInfo, error); + } + + /** + * Gets the error text of this node. + * + * @return The error text. + */ + public CharSequence getError() { + return IMPL.getError(mInfo); + } + + /** + * Sets the view for which the view represented by this info serves as a + * label for accessibility purposes. + * + * @param labeled The view for which this info serves as a label. + */ + public void setLabelFor(View labeled) { + IMPL.setLabelFor(mInfo, labeled); + } + + /** + * Sets the view for which the view represented by this info serves as a + * label for accessibility purposes. If virtualDescendantId + * is {@link View#NO_ID} the root is set as the labeled. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root whose virtual descendant serves as a label. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setLabelFor(View root, int virtualDescendantId) { + IMPL.setLabelFor(mInfo, root, virtualDescendantId); + } + + @Override + public int hashCode() { + return (mInfo == null) ? 0 : mInfo.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityNodeInfoCompat other = (AccessibilityNodeInfoCompat) obj; + if (mInfo == null) { + if (other.mInfo != null) { + return false; + } + } else if (!mInfo.equals(other.mInfo)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(super.toString()); + + Rect bounds = new Rect(); + + getBoundsInParent(bounds); + builder.append("; boundsInParent: " + bounds); + + getBoundsInScreen(bounds); + builder.append("; boundsInScreen: " + bounds); + + builder.append("; packageName: ").append(getPackageName()); + builder.append("; className: ").append(getClassName()); + builder.append("; text: ").append(getText()); + builder.append("; contentDescription: ").append(getContentDescription()); + builder.append("; viewId: ").append(getViewIdResourceName()); + + builder.append("; checkable: ").append(isCheckable()); + builder.append("; checked: ").append(isChecked()); + builder.append("; focusable: ").append(isFocusable()); + builder.append("; focused: ").append(isFocused()); + builder.append("; selected: ").append(isSelected()); + builder.append("; clickable: ").append(isClickable()); + builder.append("; longClickable: ").append(isLongClickable()); + builder.append("; enabled: ").append(isEnabled()); + builder.append("; password: ").append(isPassword()); + builder.append("; scrollable: " + isScrollable()); + + builder.append("; ["); + for (int actionBits = getActions(); actionBits != 0;) { + final int action = 1 << Integer.numberOfTrailingZeros(actionBits); + actionBits &= ~action; + builder.append(getActionSymbolicName(action)); + if (actionBits != 0) { + builder.append(", "); + } + } + builder.append("]"); + + return builder.toString(); + } + + private static String getActionSymbolicName(int action) { + switch (action) { + case ACTION_FOCUS: + return "ACTION_FOCUS"; + case ACTION_CLEAR_FOCUS: + return "ACTION_CLEAR_FOCUS"; + case ACTION_SELECT: + return "ACTION_SELECT"; + case ACTION_CLEAR_SELECTION: + return "ACTION_CLEAR_SELECTION"; + case ACTION_CLICK: + return "ACTION_CLICK"; + case ACTION_LONG_CLICK: + return "ACTION_LONG_CLICK"; + case ACTION_ACCESSIBILITY_FOCUS: + return "ACTION_ACCESSIBILITY_FOCUS"; + case ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return "ACTION_CLEAR_ACCESSIBILITY_FOCUS"; + case ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + return "ACTION_NEXT_AT_MOVEMENT_GRANULARITY"; + case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + return "ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY"; + case ACTION_NEXT_HTML_ELEMENT: + return "ACTION_NEXT_HTML_ELEMENT"; + case ACTION_PREVIOUS_HTML_ELEMENT: + return "ACTION_PREVIOUS_HTML_ELEMENT"; + case ACTION_SCROLL_FORWARD: + return "ACTION_SCROLL_FORWARD"; + case ACTION_SCROLL_BACKWARD: + return "ACTION_SCROLL_BACKWARD"; + case ACTION_CUT: + return "ACTION_CUT"; + case ACTION_COPY: + return "ACTION_COPY"; + case ACTION_PASTE: + return "ACTION_PASTE"; + case ACTION_SET_SELECTION: + return "ACTION_SET_SELECTION"; + default: + return"ACTION_UNKNOWN"; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java new file mode 100644 index 00000000000..ce6abf3078f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java @@ -0,0 +1,83 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import java.util.List; + +/** + * Api21-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatApi21 { + static List getActionList(Object info) { + Object result = ((AccessibilityNodeInfo) info).getActionList(); + return (List) result; + } + + static void addAction(Object info, Object action) { + ((AccessibilityNodeInfo) info).addAction((AccessibilityAction) action); + } + + public static Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfo.CollectionInfo.obtain(rowCount, columnCount, hierarchical, + selectionMode); + } + + public static Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex, + columnSpan, heading, selected); + } + + public static CharSequence getError(Object info) { + return ((AccessibilityNodeInfo) info).getError(); + } + + public static void setError(Object info, CharSequence error) { + ((AccessibilityNodeInfo) info).setError(error); + } + + public static void setLabelFor(Object info, View labeled) { + ((AccessibilityNodeInfo) info).setLabelFor(labeled); + } + + public static void setLabelFor(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setLabelFor(root, virtualDescendantId); + } + + static class CollectionItemInfo { + public static boolean isSelected(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).isSelected(); + } + } + + static Object newAccessibilityAction(int actionId, CharSequence label) { + return new AccessibilityAction(actionId, label); + } + + static int getAccessibilityActionId(Object action) { + return ((AccessibilityNodeInfo.AccessibilityAction) action).getId(); + } + + static CharSequence getAccessibilityActionLabel(Object action) { + return ((AccessibilityNodeInfo.AccessibilityAction) action).getLabel(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java new file mode 100644 index 00000000000..786318d9974 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.View; + +/** + * Api22-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatApi22 { + + public static Object getTraversalBefore(Object info) { + return ((AccessibilityNodeInfo) info).getTraversalBefore(); + } + + public static void setTraversalBefore(Object info, View view) { + ((AccessibilityNodeInfo) info).setTraversalBefore(view); + } + + public static void setTraversalBefore(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setTraversalBefore(root, virtualDescendantId); + } + + public static Object getTraversalAfter(Object info) { + return ((AccessibilityNodeInfo) info).getTraversalAfter(); + } + + public static void setTraversalAfter(Object info, View view) { + ((AccessibilityNodeInfo) info).setTraversalAfter(view); + } + + public static void setTraversalAfter(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setTraversalAfter(root, virtualDescendantId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java new file mode 100644 index 00000000000..367f995b03f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.graphics.Rect; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.util.List; + +/** + * ICS specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatIcs { + + public static Object obtain() { + return AccessibilityNodeInfo.obtain(); + } + + public static Object obtain(View source) { + return AccessibilityNodeInfo.obtain(source); + } + + public static Object obtain(Object info) { + return AccessibilityNodeInfo.obtain((AccessibilityNodeInfo) info); + } + + public static void addAction(Object info, int action) { + ((AccessibilityNodeInfo) info).addAction(action); + } + + public static void addChild(Object info, View child) { + ((AccessibilityNodeInfo) info).addChild(child); + } + + @SuppressWarnings("unchecked") + public static List findAccessibilityNodeInfosByText(Object info, String text) { + Object result = ((AccessibilityNodeInfo) info).findAccessibilityNodeInfosByText(text); + return (List) result; + } + + public static int getActions(Object info) { + return ((AccessibilityNodeInfo) info).getActions(); + } + + public static void getBoundsInParent(Object info, Rect outBounds) { + ((AccessibilityNodeInfo) info).getBoundsInParent(outBounds); + } + + public static void getBoundsInScreen(Object info, Rect outBounds) { + ((AccessibilityNodeInfo) info).getBoundsInScreen(outBounds); + } + + public static Object getChild(Object info, int index) { + return ((AccessibilityNodeInfo) info).getChild(index); + } + + public static int getChildCount(Object info) { + return ((AccessibilityNodeInfo) info).getChildCount(); + } + + public static CharSequence getClassName(Object info) { + return ((AccessibilityNodeInfo) info).getClassName(); + } + + public static CharSequence getContentDescription(Object info) { + return ((AccessibilityNodeInfo) info).getContentDescription(); + } + + public static CharSequence getPackageName(Object info) { + return ((AccessibilityNodeInfo) info).getPackageName(); + } + + public static Object getParent(Object info) { + return ((AccessibilityNodeInfo) info).getParent(); + } + + public static CharSequence getText(Object info) { + return ((AccessibilityNodeInfo) info).getText(); + } + + public static int getWindowId(Object info) { + return ((AccessibilityNodeInfo) info).getWindowId(); + } + + public static boolean isCheckable(Object info) { + return ((AccessibilityNodeInfo) info).isCheckable(); + } + + public static boolean isChecked(Object info) { + return ((AccessibilityNodeInfo) info).isChecked(); + } + + public static boolean isClickable(Object info) { + return ((AccessibilityNodeInfo) info).isClickable(); + } + + public static boolean isEnabled(Object info) { + return ((AccessibilityNodeInfo) info).isEnabled(); + } + + public static boolean isFocusable(Object info) { + return ((AccessibilityNodeInfo) info).isFocusable(); + } + + public static boolean isFocused(Object info) { + return ((AccessibilityNodeInfo) info).isFocused(); + } + + public static boolean isLongClickable(Object info) { + return ((AccessibilityNodeInfo) info).isLongClickable(); + } + + public static boolean isPassword(Object info) { + return ((AccessibilityNodeInfo) info).isPassword(); + } + + public static boolean isScrollable(Object info) { + return ((AccessibilityNodeInfo) info).isScrollable(); + } + + public static boolean isSelected(Object info) { + return ((AccessibilityNodeInfo) info).isSelected(); + } + + public static boolean performAction(Object info, int action) { + return ((AccessibilityNodeInfo) info).performAction(action); + } + + public static void setBoundsInParent(Object info, Rect bounds) { + ((AccessibilityNodeInfo) info).setBoundsInParent(bounds); + } + + public static void setBoundsInScreen(Object info, Rect bounds) { + ((AccessibilityNodeInfo) info).setBoundsInScreen(bounds); + } + + public static void setCheckable(Object info, boolean checkable) { + ((AccessibilityNodeInfo) info).setCheckable(checkable); + } + + public static void setChecked(Object info, boolean checked) { + ((AccessibilityNodeInfo) info).setChecked(checked); + } + + public static void setClassName(Object info, CharSequence className) { + ((AccessibilityNodeInfo) info).setClassName(className); + } + + public static void setClickable(Object info, boolean clickable) { + ((AccessibilityNodeInfo) info).setClickable(clickable); + } + + public static void setContentDescription(Object info, CharSequence contentDescription) { + ((AccessibilityNodeInfo) info).setContentDescription(contentDescription); + } + + public static void setEnabled(Object info, boolean enabled) { + ((AccessibilityNodeInfo) info).setEnabled(enabled); + } + + public static void setFocusable(Object info, boolean focusable) { + ((AccessibilityNodeInfo) info).setFocusable(focusable); + } + + public static void setFocused(Object info, boolean focused) { + ((AccessibilityNodeInfo) info).setFocused(focused); + } + + public static void setLongClickable(Object info, boolean longClickable) { + ((AccessibilityNodeInfo) info).setLongClickable(longClickable); + } + + public static void setPackageName(Object info, CharSequence packageName) { + ((AccessibilityNodeInfo) info).setPackageName(packageName); + } + + public static void setParent(Object info, View parent) { + ((AccessibilityNodeInfo) info).setParent(parent); + } + + public static void setPassword(Object info, boolean password) { + ((AccessibilityNodeInfo) info).setPassword(password); + } + + public static void setScrollable(Object info, boolean scrollable) { + ((AccessibilityNodeInfo) info).setScrollable(scrollable); + } + + public static void setSelected(Object info, boolean selected) { + ((AccessibilityNodeInfo) info).setSelected(selected); + } + + public static void setSource(Object info, View source) { + ((AccessibilityNodeInfo) info).setSource(source); + } + + public static void setText(Object info, CharSequence text) { + ((AccessibilityNodeInfo) info).setText(text); + } + + public static void recycle(Object info) { + ((AccessibilityNodeInfo) info).recycle(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java new file mode 100644 index 00000000000..c2f4d938fa5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * JellyBean specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatJellyBean { + + public static void addChild(Object info, View child, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).addChild(child, virtualDescendantId); + } + + public static void setSource(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setSource(root, virtualDescendantId); + } + + public static boolean isVisibleToUser(Object info) { + return ((AccessibilityNodeInfo) info).isVisibleToUser(); + } + + public static void setVisibleToUser(Object info, boolean visibleToUser) { + ((AccessibilityNodeInfo) info).setVisibleToUser(visibleToUser); + } + + public static boolean performAction(Object info, int action, Bundle arguments) { + return ((AccessibilityNodeInfo) info).performAction(action, arguments); + } + + public static void setMovementGranularities(Object info, int granularities) { + ((AccessibilityNodeInfo) info).setMovementGranularities(granularities); + } + + public static int getMovementGranularities(Object info) { + return ((AccessibilityNodeInfo) info).getMovementGranularities(); + } + + public static Object obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfo.obtain(root, virtualDescendantId); + } + + public static Object findFocus(Object info, int focus) { + return ((AccessibilityNodeInfo) info).findFocus(focus); + } + + public static Object focusSearch(Object info, int direction) { + return ((AccessibilityNodeInfo) info).focusSearch(direction); + } + + public static void setParent(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setParent(root, virtualDescendantId); + } + + public static boolean isAccessibilityFocused(Object info) { + return ((AccessibilityNodeInfo) info).isAccessibilityFocused(); + } + + public static void setAccesibilityFocused(Object info, boolean focused) { + ((AccessibilityNodeInfo) info).setAccessibilityFocused(focused); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java new file mode 100644 index 00000000000..2b76ea0a10b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; + +class AccessibilityNodeInfoCompatJellybeanMr2 { + + public static void setViewIdResourceName(Object info, String viewId) { + ((AccessibilityNodeInfo) info).setViewIdResourceName(viewId); + } + + public static String getViewIdResourceName(Object info) { + return ((AccessibilityNodeInfo) info).getViewIdResourceName(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java new file mode 100644 index 00000000000..32fb86fb3bd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java @@ -0,0 +1,127 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * KitKat-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatKitKat { + static int getLiveRegion(Object info) { + return ((AccessibilityNodeInfo) info).getLiveRegion(); + } + + static void setLiveRegion(Object info, int mode) { + ((AccessibilityNodeInfo) info).setLiveRegion(mode); + } + + static Object getCollectionInfo(Object info) { + return ((AccessibilityNodeInfo) info).getCollectionInfo(); + } + + static Object getCollectionItemInfo(Object info) { + return ((AccessibilityNodeInfo) info).getCollectionItemInfo(); + } + + public static void setCollectionInfo(Object info, Object collectionInfo) { + ((AccessibilityNodeInfo) info).setCollectionInfo( + (AccessibilityNodeInfo.CollectionInfo)collectionInfo); + } + + public static void setCollectionItemInfo(Object info, Object collectionItemInfo) { + ((AccessibilityNodeInfo) info).setCollectionItemInfo( + (AccessibilityNodeInfo.CollectionItemInfo) collectionItemInfo); + } + + static Object getRangeInfo(Object info) { + return ((AccessibilityNodeInfo) info).getRangeInfo(); + } + + public static Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfo.CollectionInfo.obtain(rowCount, columnCount, hierarchical); + } + + public static Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading) { + return AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex, + columnSpan, heading); + } + + public static void setContentInvalid(Object info, boolean contentInvalid) { + ((AccessibilityNodeInfo) info).setContentInvalid(contentInvalid); + } + + public static boolean isContentInvalid(Object info) { + return ((AccessibilityNodeInfo) info).isContentInvalid(); + } + + static class CollectionInfo { + static int getColumnCount(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).getColumnCount(); + } + + static int getRowCount(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).getRowCount(); + } + + static boolean isHierarchical(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).isHierarchical(); + } + } + + static class CollectionItemInfo { + static int getColumnIndex(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnIndex(); + } + + static int getColumnSpan(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnSpan(); + } + + static int getRowIndex(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowIndex(); + } + + static int getRowSpan(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowSpan(); + } + + static boolean isHeading(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).isHeading(); + } + } + + static class RangeInfo { + static float getCurrent(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getCurrent(); + } + + static float getMax(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getMax(); + } + + static float getMin(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getMin(); + } + + static int getType(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getType(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java new file mode 100644 index 00000000000..e6da5f1a0bd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java @@ -0,0 +1,248 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityNodeProvider} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityNodeProviderCompat { + + interface AccessibilityNodeProviderImpl { + public Object newAccessibilityNodeProviderBridge(AccessibilityNodeProviderCompat compat); + } + + static class AccessibilityNodeProviderStubImpl implements AccessibilityNodeProviderImpl { + @Override + public Object newAccessibilityNodeProviderBridge(AccessibilityNodeProviderCompat compat) { + return null; + } + } + + static class AccessibilityNodeProviderJellyBeanImpl extends AccessibilityNodeProviderStubImpl { + @Override + public Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeProviderCompat compat) { + return AccessibilityNodeProviderCompatJellyBean.newAccessibilityNodeProviderBridge( + new AccessibilityNodeProviderCompatJellyBean.AccessibilityNodeInfoBridge() { + @Override + public boolean performAction(int virtualViewId, int action, + Bundle arguments) { + return compat.performAction(virtualViewId, action, arguments); + } + + @Override + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + List compatInfos = + compat.findAccessibilityNodeInfosByText(text, virtualViewId); + List infos = new ArrayList(); + final int infoCount = compatInfos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityNodeInfoCompat infoCompat = compatInfos.get(i); + infos.add(infoCompat.getInfo()); + } + return infos; + } + + @Override + public Object createAccessibilityNodeInfo( + int virtualViewId) { + final AccessibilityNodeInfoCompat compatInfo = compat + .createAccessibilityNodeInfo(virtualViewId); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + }); + } + } + + static class AccessibilityNodeProviderKitKatImpl extends AccessibilityNodeProviderStubImpl { + @Override + public Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeProviderCompat compat) { + return AccessibilityNodeProviderCompatKitKat.newAccessibilityNodeProviderBridge( + new AccessibilityNodeProviderCompatKitKat.AccessibilityNodeInfoBridge() { + @Override + public boolean performAction( + int virtualViewId, int action, Bundle arguments) { + return compat.performAction(virtualViewId, action, arguments); + } + + @Override + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + List compatInfos = + compat.findAccessibilityNodeInfosByText(text, virtualViewId); + List infos = new ArrayList(); + final int infoCount = compatInfos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityNodeInfoCompat infoCompat = compatInfos.get(i); + infos.add(infoCompat.getInfo()); + } + return infos; + } + + @Override + public Object createAccessibilityNodeInfo(int virtualViewId) { + final AccessibilityNodeInfoCompat compatInfo = + compat.createAccessibilityNodeInfo(virtualViewId); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + + @Override + public Object findFocus(int focus) { + final AccessibilityNodeInfoCompat compatInfo = compat.findFocus(focus); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + }); + } + } + + private static final AccessibilityNodeProviderImpl IMPL; + + private final Object mProvider; + + static { + if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityNodeProviderKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityNodeProviderJellyBeanImpl(); + } else { + IMPL = new AccessibilityNodeProviderStubImpl(); + } + } + + /** + * Creates a new instance. + */ + public AccessibilityNodeProviderCompat() { + mProvider = IMPL.newAccessibilityNodeProviderBridge(this); + } + + /** + * Creates a new instance wrapping an + * {@link android.view.accessibility.AccessibilityNodeProvider}. + * + * @param provider The provider. + */ + public AccessibilityNodeProviderCompat(Object provider) { + mProvider = provider; + } + + /** + * @return The wrapped {@link android.view.accessibility.AccessibilityNodeProvider}. + */ + public Object getProvider() { + return mProvider; + } + + /** + * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual view, + * i.e. a descendant of the host View, with the given virtualViewId + * or the host View itself if virtualViewId equals to {@link View#NO_ID}. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * The implementer is responsible for obtaining an accessibility node info from the + * pool of reusable instances and setting the desired properties of the node info + * before returning it. + *

    + * + * @param virtualViewId A client defined virtual view id. + * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant + * or the host View. + * + * @see AccessibilityNodeInfoCompat + */ + public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { + return null; + } + + /** + * Performs an accessibility action on a virtual view, i.e. a descendant of the + * host View, with the given virtualViewId or the host View itself + * if virtualViewId equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id. + * @param action The action to perform. + * @param arguments Optional arguments. + * @return True if the action was performed. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfoCompat + */ + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return false; + } + + /** + * Finds {@link AccessibilityNodeInfoCompat}s by text. The match is case insensitive + * containment. The search is relative to the virtual view, i.e. a descendant of the + * host View, with the given virtualViewId or the host View itself + * virtualViewId equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @param text The searched text. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfoCompat + */ + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return null; + } + + /** + * Find the virtual view, i.e. a descendant of the host View, that has the + * specified focus type. + * + * @param focus The focus to find. One of + * {@link AccessibilityNodeInfoCompat#FOCUS_INPUT} or + * {@link AccessibilityNodeInfoCompat#FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * @see AccessibilityNodeInfoCompat#FOCUS_INPUT + * @see AccessibilityNodeInfoCompat#FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfoCompat findFocus(int focus) { + return null; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java new file mode 100644 index 00000000000..cb8a3b4ece8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java @@ -0,0 +1,59 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.List; + +/** + * JellyBean specific AccessibilityNodeProvider API implementation. + */ +class AccessibilityNodeProviderCompatJellyBean { + interface AccessibilityNodeInfoBridge { + public Object createAccessibilityNodeInfo(int virtualViewId); + public boolean performAction(int virtualViewId, int action, Bundle arguments); + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId); + } + + public static Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeInfoBridge bridge) { + return new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return (AccessibilityNodeInfo) bridge.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + @SuppressWarnings("unchecked") + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + // Use some voodoo to avoid creating intermediary instances. + return (List) (List) + bridge.findAccessibilityNodeInfosByText(text, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return bridge.performAction(virtualViewId, action, arguments); + } + }; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java new file mode 100644 index 00000000000..3aa475dfd2b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.List; + +/** + * KitKat-specific AccessibilityNodeProvider API implementation. + */ +class AccessibilityNodeProviderCompatKitKat { + interface AccessibilityNodeInfoBridge { + public Object createAccessibilityNodeInfo(int virtualViewId); + public boolean performAction(int virtualViewId, int action, Bundle arguments); + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId); + public Object findFocus(int focus); + } + + public static Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeInfoBridge bridge) { + return new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return (AccessibilityNodeInfo) bridge.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + @SuppressWarnings("unchecked") + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + // Use some voodoo to avoid creating intermediary instances. + return (List) (List) + bridge.findAccessibilityNodeInfosByText(text, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return bridge.performAction(virtualViewId, action, arguments); + } + + @Override + public AccessibilityNodeInfo findFocus(int focus) { + return (AccessibilityNodeInfo) bridge.findFocus(focus); + } + }; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java new file mode 100644 index 00000000000..f5655ccda15 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java @@ -0,0 +1,1044 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.os.Build; +import android.os.Parcelable; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityRecord} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityRecordCompat { + + static interface AccessibilityRecordImpl { + public Object obtain(); + public Object obtain(Object record); + public void setSource(Object record, View source); + public void setSource(Object record, View root, int virtualDescendantId); + public AccessibilityNodeInfoCompat getSource(Object record); + public int getWindowId(Object record); + public boolean isChecked(Object record); + public void setChecked(Object record, boolean isChecked); + public boolean isEnabled(Object record); + public void setEnabled(Object record, boolean isEnabled); + public boolean isPassword(Object record); + public void setPassword(Object record, boolean isPassword); + public boolean isFullScreen(Object record); + public void setFullScreen(Object record, boolean isFullScreen); + public boolean isScrollable(Object record); + public void setScrollable(Object record, boolean scrollable); + public int getItemCount(Object record); + public void setItemCount(Object record, int itemCount); + public int getCurrentItemIndex(Object record); + public void setCurrentItemIndex(Object record, int currentItemIndex); + public int getFromIndex(Object record); + public void setFromIndex(Object record, int fromIndex); + public int getToIndex(Object record); + public void setToIndex(Object record, int toIndex); + public int getScrollX(Object record); + public void setScrollX(Object record, int scrollX); + public int getScrollY(Object record); + public void setScrollY(Object record, int scrollY); + public int getMaxScrollX(Object record); + public void setMaxScrollX(Object record, int maxScrollX); + public int getMaxScrollY(Object record); + public void setMaxScrollY(Object record, int maxScrollY); + public int getAddedCount(Object record); + public void setAddedCount(Object record, int addedCount); + public int getRemovedCount(Object record); + public void setRemovedCount(Object record, int removedCount); + public CharSequence getClassName(Object record); + public void setClassName(Object record, CharSequence className); + public List getText(Object record); + public CharSequence getBeforeText(Object record); + public void setBeforeText(Object record, CharSequence beforeText); + public CharSequence getContentDescription(Object record); + public void setContentDescription(Object record, CharSequence contentDescription); + public Parcelable getParcelableData(Object record); + public void setParcelableData(Object record, Parcelable parcelableData); + public void recycle(Object record); + } + + static class AccessibilityRecordStubImpl implements AccessibilityRecordImpl { + public Object obtain() { + return null; + } + + public Object obtain(Object record) { + return null; + } + + public int getAddedCount(Object record) { + return 0; + } + + public CharSequence getBeforeText(Object record) { + return null; + } + + public CharSequence getClassName(Object record) { + return null; + } + + public CharSequence getContentDescription(Object record) { + return null; + } + + public int getCurrentItemIndex(Object record) { + return 0; + } + + public int getFromIndex(Object record) { + return 0; + } + + public int getItemCount(Object record) { + return 0; + } + + public int getMaxScrollX(Object record) { + return 0; + } + + public int getMaxScrollY(Object record) { + return 0; + } + + public Parcelable getParcelableData(Object record) { + return null; + } + + public int getRemovedCount(Object record) { + return 0; + } + + public int getScrollX(Object record) { + return 0; + } + + public int getScrollY(Object record) { + return 0; + } + + public AccessibilityNodeInfoCompat getSource(Object record) { + return null; + } + + public List getText(Object record) { + return Collections.emptyList(); + } + + public int getToIndex(Object record) { + return 0; + } + + public int getWindowId(Object record) { + return 0; + } + + public boolean isChecked(Object record) { + return false; + } + + public boolean isEnabled(Object record) { + return false; + } + + public boolean isFullScreen(Object record) { + return false; + } + + public boolean isPassword(Object record) { + return false; + } + + public boolean isScrollable(Object record) { + return false; + } + + public void recycle(Object record) { + + } + + public void setAddedCount(Object record, int addedCount) { + + } + + public void setBeforeText(Object record, CharSequence beforeText) { + + } + + public void setChecked(Object record, boolean isChecked) { + + } + + public void setClassName(Object record, CharSequence className) { + + } + + public void setContentDescription(Object record, CharSequence contentDescription) { + + } + + public void setCurrentItemIndex(Object record, int currentItemIndex) { + + } + + public void setEnabled(Object record, boolean isEnabled) { + + } + + public void setFromIndex(Object record, int fromIndex) { + + } + + public void setFullScreen(Object record, boolean isFullScreen) { + + } + + public void setItemCount(Object record, int itemCount) { + + } + + public void setMaxScrollX(Object record, int maxScrollX) { + + } + + public void setMaxScrollY(Object record, int maxScrollY) { + + } + + public void setParcelableData(Object record, Parcelable parcelableData) { + + } + + public void setPassword(Object record, boolean isPassword) { + + } + + public void setRemovedCount(Object record, int removedCount) { + + } + + public void setScrollX(Object record, int scrollX) { + + } + + public void setScrollY(Object record, int scrollY) { + + } + + public void setScrollable(Object record, boolean scrollable) { + + } + + public void setSource(Object record, View source) { + + } + + public void setSource(Object record, View root, int virtualDescendantId) { + + } + + public void setToIndex(Object record, int toIndex) { + + } + } + + static class AccessibilityRecordIcsImpl extends AccessibilityRecordStubImpl { + @Override + public Object obtain() { + return AccessibilityRecordCompatIcs.obtain(); + } + + @Override + public Object obtain(Object record) { + return AccessibilityRecordCompatIcs.obtain(record); + } + + @Override + public int getAddedCount(Object record) { + return AccessibilityRecordCompatIcs.getAddedCount(record); + } + + @Override + public CharSequence getBeforeText(Object record) { + return AccessibilityRecordCompatIcs.getBeforeText(record); + } + + @Override + public CharSequence getClassName(Object record) { + return AccessibilityRecordCompatIcs.getClassName(record); + } + + @Override + public CharSequence getContentDescription(Object record) { + return AccessibilityRecordCompatIcs.getContentDescription(record); + } + + @Override + public int getCurrentItemIndex(Object record) { + return AccessibilityRecordCompatIcs.getCurrentItemIndex(record); + } + + @Override + public int getFromIndex(Object record) { + return AccessibilityRecordCompatIcs.getFromIndex(record); + } + + @Override + public int getItemCount(Object record) { + return AccessibilityRecordCompatIcs.getItemCount(record); + } + + @Override + public Parcelable getParcelableData(Object record) { + return AccessibilityRecordCompatIcs.getParcelableData(record); + } + + @Override + public int getRemovedCount(Object record) { + return AccessibilityRecordCompatIcs.getRemovedCount(record); + } + + @Override + public int getScrollX(Object record) { + return AccessibilityRecordCompatIcs.getScrollX(record); + } + + @Override + public int getScrollY(Object record) { + return AccessibilityRecordCompatIcs.getScrollY(record); + } + + @Override + public AccessibilityNodeInfoCompat getSource(Object record) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance( + AccessibilityRecordCompatIcs.getSource(record)); + } + + @Override + public List getText(Object record) { + return AccessibilityRecordCompatIcs.getText(record); + } + + @Override + public int getToIndex(Object record) { + return AccessibilityRecordCompatIcs.getToIndex(record); + } + + @Override + public int getWindowId(Object record) { + return AccessibilityRecordCompatIcs.getWindowId(record); + } + + @Override + public boolean isChecked(Object record) { + return AccessibilityRecordCompatIcs.isChecked(record); + } + + @Override + public boolean isEnabled(Object record) { + return AccessibilityRecordCompatIcs.isEnabled(record); + } + + @Override + public boolean isFullScreen(Object record) { + return AccessibilityRecordCompatIcs.isFullScreen(record); + } + + @Override + public boolean isPassword(Object record) { + return AccessibilityRecordCompatIcs.isPassword(record); + } + + @Override + public boolean isScrollable(Object record) { + return AccessibilityRecordCompatIcs.isScrollable(record); + } + + @Override + public void recycle(Object record) { + AccessibilityRecordCompatIcs.recycle(record); + } + + @Override + public void setAddedCount(Object record, int addedCount) { + AccessibilityRecordCompatIcs.setAddedCount(record, addedCount); + } + + @Override + public void setBeforeText(Object record, CharSequence beforeText) { + AccessibilityRecordCompatIcs.setBeforeText(record, beforeText); + } + + @Override + public void setChecked(Object record, boolean isChecked) { + AccessibilityRecordCompatIcs.setChecked(record, isChecked); + } + + @Override + public void setClassName(Object record, CharSequence className) { + AccessibilityRecordCompatIcs.setClassName(record, className); + } + + @Override + public void setContentDescription(Object record, CharSequence contentDescription) { + AccessibilityRecordCompatIcs.setContentDescription(record, contentDescription); + } + + @Override + public void setCurrentItemIndex(Object record, int currentItemIndex) { + AccessibilityRecordCompatIcs.setCurrentItemIndex(record, currentItemIndex); + } + + @Override + public void setEnabled(Object record, boolean isEnabled) { + AccessibilityRecordCompatIcs.setEnabled(record, isEnabled); + } + + @Override + public void setFromIndex(Object record, int fromIndex) { + AccessibilityRecordCompatIcs.setFromIndex(record, fromIndex); + } + + @Override + public void setFullScreen(Object record, boolean isFullScreen) { + AccessibilityRecordCompatIcs.setFullScreen(record, isFullScreen); + } + + @Override + public void setItemCount(Object record, int itemCount) { + AccessibilityRecordCompatIcs.setItemCount(record, itemCount); + } + + @Override + public void setParcelableData(Object record, Parcelable parcelableData) { + AccessibilityRecordCompatIcs.setParcelableData(record, parcelableData); + } + + @Override + public void setPassword(Object record, boolean isPassword) { + AccessibilityRecordCompatIcs.setPassword(record, isPassword); + } + + @Override + public void setRemovedCount(Object record, int removedCount) { + AccessibilityRecordCompatIcs.setRemovedCount(record, removedCount); + } + + @Override + public void setScrollX(Object record, int scrollX) { + AccessibilityRecordCompatIcs.setScrollX(record, scrollX); + } + + @Override + public void setScrollY(Object record, int scrollY) { + AccessibilityRecordCompatIcs.setScrollY(record, scrollY); + } + + @Override + public void setScrollable(Object record, boolean scrollable) { + AccessibilityRecordCompatIcs.setScrollable(record, scrollable); + } + + @Override + public void setSource(Object record, View source) { + AccessibilityRecordCompatIcs.setSource(record, source); + } + + @Override + public void setToIndex(Object record, int toIndex) { + AccessibilityRecordCompatIcs.setToIndex(record, toIndex); + } + } + + static class AccessibilityRecordIcsMr1Impl extends AccessibilityRecordIcsImpl { + @Override + public int getMaxScrollX(Object record) { + return AccessibilityRecordCompatIcsMr1.getMaxScrollX(record); + } + + @Override + public int getMaxScrollY(Object record) { + return AccessibilityRecordCompatIcsMr1.getMaxScrollY(record); + } + + @Override + public void setMaxScrollX(Object record, int maxScrollX) { + AccessibilityRecordCompatIcsMr1.setMaxScrollX(record, maxScrollX); + } + + @Override + public void setMaxScrollY(Object record, int maxScrollY) { + AccessibilityRecordCompatIcsMr1.setMaxScrollY(record, maxScrollY); + } + } + + static class AccessibilityRecordJellyBeanImpl extends AccessibilityRecordIcsMr1Impl { + @Override + public void setSource(Object record, View root, int virtualDescendantId) { + AccessibilityRecordCompatJellyBean.setSource(record, root, virtualDescendantId); + } + } + + static { + if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityRecordJellyBeanImpl(); + } else if (Build.VERSION.SDK_INT >= 15) { // ICS MR1 + IMPL = new AccessibilityRecordIcsMr1Impl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityRecordIcsImpl(); + } else { + IMPL = new AccessibilityRecordStubImpl(); + } + } + + private static final AccessibilityRecordImpl IMPL; + + private final Object mRecord; + + /** + * @deprecated This is not type safe. If you want to modify an + * {@link AccessibilityEvent}'s properties defined in + * {@link android.view.accessibility.AccessibilityRecord} use + * {@link AccessibilityEventCompat#asRecord(AccessibilityEvent)}. This method will be removed + * in a subsequent release of the support library. + */ + public AccessibilityRecordCompat(Object record) { + mRecord = record; + } + + /** + * @return The wrapped implementation. + * + * @deprecated This method will be removed in a subsequent release of + * the support library. + */ + public Object getImpl() { + return mRecord; + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. The instance is initialized with data from the + * given record. + * + * @return An instance. + */ + public static AccessibilityRecordCompat obtain(AccessibilityRecordCompat record) { + return new AccessibilityRecordCompat(IMPL.obtain(record.mRecord)); + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. + * + * @return An instance. + */ + public static AccessibilityRecordCompat obtain() { + return new AccessibilityRecordCompat(IMPL.obtain()); + } + + /** + * Sets the event source. + * + * @param source The source. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSource(View source) { + IMPL.setSource(mRecord, source); + } + + /** + * Sets the source to be a virtual descendant of the given root. + * If virtualDescendantId equals to {@link View#NO_ID} the root + * is set as the source. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { + IMPL.setSource(mRecord, root, virtualDescendantId); + } + + /** + * Gets the {@link android.view.accessibility.AccessibilityNodeInfo} of + * the event source. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling + * {@link android.view.accessibility.AccessibilityNodeInfo#recycle() + * AccessibilityNodeInfo#recycle()} to avoid creating of multiple instances. + *

    + * + * @return The info of the source. + */ + public AccessibilityNodeInfoCompat getSource() { + return IMPL.getSource(mRecord); + } + + /** + * Gets the id of the window from which the event comes from. + * + * @return The window id. + */ + public int getWindowId() { + return IMPL.getWindowId(mRecord); + } + + /** + * Gets if the source is checked. + * + * @return True if the view is checked, false otherwise. + */ + public boolean isChecked() { + return IMPL.isChecked(mRecord); + } + + /** + * Sets if the source is checked. + * + * @param isChecked True if the view is checked, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean isChecked) { + IMPL.setChecked(mRecord, isChecked); + } + + /** + * Gets if the source is enabled. + * + * @return True if the view is enabled, false otherwise. + */ + public boolean isEnabled() { + return IMPL.isEnabled(mRecord); + } + + /** + * Sets if the source is enabled. + * + * @param isEnabled True if the view is enabled, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean isEnabled) { + IMPL.setEnabled(mRecord, isEnabled); + } + + /** + * Gets if the source is a password field. + * + * @return True if the view is a password field, false otherwise. + */ + public boolean isPassword() { + return IMPL.isPassword(mRecord); + } + + /** + * Sets if the source is a password field. + * + * @param isPassword True if the view is a password field, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean isPassword) { + IMPL.setPassword(mRecord, isPassword); + } + + /** + * Gets if the source is taking the entire screen. + * + * @return True if the source is full screen, false otherwise. + */ + public boolean isFullScreen() { + return IMPL.isFullScreen(mRecord); + } + + /** + * Sets if the source is taking the entire screen. + * + * @param isFullScreen True if the source is full screen, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFullScreen(boolean isFullScreen) { + IMPL.setFullScreen(mRecord, isFullScreen); + } + + /** + * Gets if the source is scrollable. + * + * @return True if the source is scrollable, false otherwise. + */ + public boolean isScrollable() { + return IMPL.isScrollable(mRecord); + } + + /** + * Sets if the source is scrollable. + * + * @param scrollable True if the source is scrollable, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setScrollable(boolean scrollable) { + IMPL.setScrollable(mRecord, scrollable); + } + + /** + * Gets the number of items that can be visited. + * + * @return The number of items. + */ + public int getItemCount() { + return IMPL.getItemCount(mRecord); + } + + /** + * Sets the number of items that can be visited. + * + * @param itemCount The number of items. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setItemCount(int itemCount) { + IMPL.setItemCount(mRecord, itemCount); + } + + /** + * Gets the index of the source in the list of items the can be visited. + * + * @return The current item index. + */ + public int getCurrentItemIndex() { + return IMPL.getCurrentItemIndex(mRecord); + } + + /** + * Sets the index of the source in the list of items that can be visited. + * + * @param currentItemIndex The current item index. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCurrentItemIndex(int currentItemIndex) { + IMPL.setCurrentItemIndex(mRecord, currentItemIndex); + } + + /** + * Gets the index of the first character of the changed sequence, + * or the beginning of a text selection or the index of the first + * visible item when scrolling. + * + * @return The index of the first character or selection + * start or the first visible item. + */ + public int getFromIndex() { + return IMPL.getFromIndex(mRecord); + } + + /** + * Sets the index of the first character of the changed sequence + * or the beginning of a text selection or the index of the first + * visible item when scrolling. + * + * @param fromIndex The index of the first character or selection + * start or the first visible item. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFromIndex(int fromIndex) { + IMPL.setFromIndex(mRecord, fromIndex); + } + + /** + * Gets the index of text selection end or the index of the last + * visible item when scrolling. + * + * @return The index of selection end or last item index. + */ + public int getToIndex() { + return IMPL.getToIndex(mRecord); + } + + /** + * Sets the index of text selection end or the index of the last + * visible item when scrolling. + * + * @param toIndex The index of selection end or last item index. + */ + public void setToIndex(int toIndex) { + IMPL.setToIndex(mRecord, toIndex); + } + + /** + * Gets the scroll offset of the source left edge in pixels. + * + * @return The scroll. + */ + public int getScrollX() { + return IMPL.getScrollX(mRecord); + } + + /** + * Sets the scroll offset of the source left edge in pixels. + * + * @param scrollX The scroll. + */ + public void setScrollX(int scrollX) { + IMPL.setScrollX(mRecord, scrollX); + } + + /** + * Gets the scroll offset of the source top edge in pixels. + * + * @return The scroll. + */ + public int getScrollY() { + return IMPL.getScrollY(mRecord); + } + + /** + * Sets the scroll offset of the source top edge in pixels. + * + * @param scrollY The scroll. + */ + public void setScrollY(int scrollY) { + IMPL.setScrollY(mRecord, scrollY); + } + + /** + * Gets the max scroll offset of the source left edge in pixels. + * + * @return The max scroll. + */ + public int getMaxScrollX() { + return IMPL.getMaxScrollX(mRecord); + } + /** + * Sets the max scroll offset of the source left edge in pixels. + * + * @param maxScrollX The max scroll. + */ + public void setMaxScrollX(int maxScrollX) { + IMPL.setMaxScrollX(mRecord, maxScrollX); + } + + /** + * Gets the max scroll offset of the source top edge in pixels. + * + * @return The max scroll. + */ + public int getMaxScrollY() { + return IMPL.getMaxScrollY(mRecord); + } + + /** + * Sets the max scroll offset of the source top edge in pixels. + * + * @param maxScrollY The max scroll. + */ + public void setMaxScrollY(int maxScrollY) { + IMPL.setMaxScrollY(mRecord, maxScrollY); + } + + /** + * Gets the number of added characters. + * + * @return The number of added characters. + */ + public int getAddedCount() { + return IMPL.getAddedCount(mRecord); + } + + /** + * Sets the number of added characters. + * + * @param addedCount The number of added characters. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAddedCount(int addedCount) { + IMPL.setAddedCount(mRecord, addedCount); + } + + /** + * Gets the number of removed characters. + * + * @return The number of removed characters. + */ + public int getRemovedCount() { + return IMPL.getRemovedCount(mRecord); + } + + /** + * Sets the number of removed characters. + * + * @param removedCount The number of removed characters. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setRemovedCount(int removedCount) { + IMPL.setRemovedCount(mRecord, removedCount); + } + + /** + * Gets the class name of the source. + * + * @return The class name. + */ + public CharSequence getClassName() { + return IMPL.getClassName(mRecord); + } + + /** + * Sets the class name of the source. + * + * @param className The lass name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + IMPL.setClassName(mRecord, className); + } + + /** + * Gets the text of the event. The index in the list represents the priority + * of the text. Specifically, the lower the index the higher the priority. + * + * @return The text. + */ + public List getText() { + return IMPL.getText(mRecord); + } + + /** + * Sets the text before a change. + * + * @return The text before the change. + */ + public CharSequence getBeforeText() { + return IMPL.getBeforeText(mRecord); + } + + /** + * Sets the text before a change. + * + * @param beforeText The text before the change. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBeforeText(CharSequence beforeText) { + IMPL.setBeforeText(mRecord, beforeText); + } + + /** + * Gets the description of the source. + * + * @return The description. + */ + public CharSequence getContentDescription() { + return IMPL.getContentDescription(mRecord); + } + + /** + * Sets the description of the source. + * + * @param contentDescription The description. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + IMPL.setContentDescription(mRecord, contentDescription); + } + + /** + * Gets the {@link Parcelable} data. + * + * @return The parcelable data. + */ + public Parcelable getParcelableData() { + return IMPL.getParcelableData(mRecord); + } + + /** + * Sets the {@link Parcelable} data of the event. + * + * @param parcelableData The parcelable data. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParcelableData(Parcelable parcelableData) { + IMPL.setParcelableData(mRecord, parcelableData); + } + + /** + * Return an instance back to be reused. + *

    + * Note: You must not touch the object after calling this + * function. + *

    + * + * @throws IllegalStateException If the record is already recycled. + */ + public void recycle() { + IMPL.recycle(mRecord); + } + + @Override + public int hashCode() { + return (mRecord == null) ? 0 : mRecord.hashCode(); + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityRecordCompat other = (AccessibilityRecordCompat) obj; + if (mRecord == null) { + if (other.mRecord != null) { + return false; + } + } else if (!mRecord.equals(other.mRecord)) { + return false; + } + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java new file mode 100644 index 00000000000..0ebcc6a2db9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.os.Parcelable; +import android.view.View; +import android.view.accessibility.AccessibilityRecord; + +import java.util.List; + +/** + * ICS specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatIcs { + + public static Object obtain() { + return AccessibilityRecord.obtain(); + } + + public static Object obtain(Object record) { + return AccessibilityRecord.obtain((AccessibilityRecord) record); + } + + public static int getAddedCount(Object record) { + return ((AccessibilityRecord) record).getAddedCount(); + } + + public static CharSequence getBeforeText(Object record) { + return ((AccessibilityRecord) record).getBeforeText(); + } + + public static CharSequence getClassName(Object record) { + return ((AccessibilityRecord) record).getClassName(); + } + + public static CharSequence getContentDescription(Object record) { + return ((AccessibilityRecord) record).getContentDescription(); + } + + public static int getCurrentItemIndex(Object record) { + return ((AccessibilityRecord) record).getCurrentItemIndex(); + } + + public static int getFromIndex(Object record) { + return ((AccessibilityRecord) record).getFromIndex(); + } + + public static int getItemCount(Object record) { + return ((AccessibilityRecord) record).getItemCount(); + } + + public static Parcelable getParcelableData(Object record) { + return ((AccessibilityRecord) record).getParcelableData(); + } + + public static int getRemovedCount(Object record) { + return ((AccessibilityRecord) record).getRemovedCount(); + } + + public static int getScrollX(Object record) { + return ((AccessibilityRecord) record).getScrollX(); + } + + public static int getScrollY(Object record) { + return ((AccessibilityRecord) record).getScrollY(); + } + + public static Object getSource(Object record) { + return ((AccessibilityRecord) record).getSource(); + } + + public static List getText(Object record) { + return ((AccessibilityRecord) record).getText(); + } + + public static int getToIndex(Object record) { + return ((AccessibilityRecord) record).getToIndex(); + } + + public static int getWindowId(Object record) { + return ((AccessibilityRecord) record).getWindowId(); + } + + public static boolean isChecked(Object record) { + return ((AccessibilityRecord) record).isChecked(); + } + + public static boolean isEnabled(Object record) { + return ((AccessibilityRecord) record).isEnabled(); + } + + public static boolean isFullScreen(Object record) { + return ((AccessibilityRecord) record).isFullScreen(); + } + + public static boolean isPassword(Object record) { + return ((AccessibilityRecord) record).isPassword(); + } + + public static boolean isScrollable(Object record) { + return ((AccessibilityRecord) record).isScrollable(); + } + + public static void recycle(Object record) { + ((AccessibilityRecord) record).recycle(); + } + + public static void setAddedCount(Object record, int addedCount) { + ((AccessibilityRecord) record).setAddedCount(addedCount); + } + + public static void setBeforeText(Object record, CharSequence beforeText) { + ((AccessibilityRecord) record).setBeforeText(beforeText); + } + + public static void setChecked(Object record, boolean isChecked) { + ((AccessibilityRecord) record).setChecked(isChecked); + } + + public static void setClassName(Object record, CharSequence className) { + ((AccessibilityRecord) record).setClassName(className); + } + + public static void setContentDescription(Object record, CharSequence contentDescription) { + ((AccessibilityRecord) record).setContentDescription(contentDescription); + } + + public static void setCurrentItemIndex(Object record, int currentItemIndex) { + ((AccessibilityRecord) record).setCurrentItemIndex(currentItemIndex); + } + + public static void setEnabled(Object record, boolean isEnabled) { + ((AccessibilityRecord) record).setEnabled(isEnabled); + } + + public static void setFromIndex(Object record, int fromIndex) { + ((AccessibilityRecord) record).setFromIndex(fromIndex); + } + + public static void setFullScreen(Object record, boolean isFullScreen) { + ((AccessibilityRecord) record).setFullScreen(isFullScreen); + } + + public static void setItemCount(Object record, int itemCount) { + ((AccessibilityRecord) record).setItemCount(itemCount); + } + + public static void setParcelableData(Object record, Parcelable parcelableData) { + ((AccessibilityRecord) record).setParcelableData(parcelableData); + } + + public static void setPassword(Object record, boolean isPassword) { + ((AccessibilityRecord) record).setPassword(isPassword); + } + + public static void setRemovedCount(Object record, int removedCount) { + ((AccessibilityRecord) record).setRemovedCount(removedCount); + } + + public static void setScrollX(Object record, int scrollX) { + ((AccessibilityRecord) record).setScrollX(scrollX); + } + + public static void setScrollY(Object record, int scrollY) { + ((AccessibilityRecord) record).setScrollY(scrollY); + } + + public static void setScrollable(Object record, boolean scrollable) { + ((AccessibilityRecord) record).setScrollable(scrollable); + } + + public static void setSource(Object record, View source) { + ((AccessibilityRecord) record).setSource(source); + } + + public static void setToIndex(Object record, int toIndex) { + ((AccessibilityRecord) record).setToIndex(toIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java new file mode 100644 index 00000000000..94164d74e00 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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.view.accessibility; + +import android.view.accessibility.AccessibilityRecord; + +/** + * ICS MR1 specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatIcsMr1 { + + public static int getMaxScrollX(Object record) { + return ((AccessibilityRecord) record).getMaxScrollX(); + } + + public static int getMaxScrollY(Object record) { + return ((AccessibilityRecord) record).getMaxScrollY(); + } + public static void setMaxScrollX(Object record, int maxScrollX) { + ((AccessibilityRecord) record).setMaxScrollX(maxScrollX); + } + + public static void setMaxScrollY(Object record, int maxScrollY) { + ((AccessibilityRecord) record).setMaxScrollY(maxScrollY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java new file mode 100644 index 00000000000..dc9d5188bf7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package android.support.v4.view.accessibility; + +import android.view.View; +import android.view.accessibility.AccessibilityRecord; + +/** + * JellyBean specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatJellyBean { + + public static void setSource(Object record, View root, int virtualDescendantId) { + ((AccessibilityRecord) record).setSource(root, virtualDescendantId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html new file mode 100644 index 00000000000..57b084f61e9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html @@ -0,0 +1,5 @@ + + +Support classes to access some of the android.view.accessibility package features introduced after API level 4 in a backwards compatible fashion. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java new file mode 100644 index 00000000000..1f39aa8390a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java @@ -0,0 +1,69 @@ +/* + * 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.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#fast_out_linear_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0.4, 0) + * P2 (1.0, 1.0) + * P3 (1.0, 1.0) + */ +public class FastOutLinearInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0008f, 0.0013f, 0.0018f, + 0.0024f, 0.0032f, 0.0040f, 0.0049f, 0.0059f, 0.0069f, 0.0081f, + 0.0093f, 0.0106f, 0.0120f, 0.0135f, 0.0151f, 0.0167f, 0.0184f, + 0.0201f, 0.0220f, 0.0239f, 0.0259f, 0.0279f, 0.0300f, 0.0322f, + 0.0345f, 0.0368f, 0.0391f, 0.0416f, 0.0441f, 0.0466f, 0.0492f, + 0.0519f, 0.0547f, 0.0574f, 0.0603f, 0.0632f, 0.0662f, 0.0692f, + 0.0722f, 0.0754f, 0.0785f, 0.0817f, 0.0850f, 0.0884f, 0.0917f, + 0.0952f, 0.0986f, 0.1021f, 0.1057f, 0.1093f, 0.1130f, 0.1167f, + 0.1205f, 0.1243f, 0.1281f, 0.1320f, 0.1359f, 0.1399f, 0.1439f, + 0.1480f, 0.1521f, 0.1562f, 0.1604f, 0.1647f, 0.1689f, 0.1732f, + 0.1776f, 0.1820f, 0.1864f, 0.1909f, 0.1954f, 0.1999f, 0.2045f, + 0.2091f, 0.2138f, 0.2184f, 0.2232f, 0.2279f, 0.2327f, 0.2376f, + 0.2424f, 0.2473f, 0.2523f, 0.2572f, 0.2622f, 0.2673f, 0.2723f, + 0.2774f, 0.2826f, 0.2877f, 0.2929f, 0.2982f, 0.3034f, 0.3087f, + 0.3141f, 0.3194f, 0.3248f, 0.3302f, 0.3357f, 0.3412f, 0.3467f, + 0.3522f, 0.3578f, 0.3634f, 0.3690f, 0.3747f, 0.3804f, 0.3861f, + 0.3918f, 0.3976f, 0.4034f, 0.4092f, 0.4151f, 0.4210f, 0.4269f, + 0.4329f, 0.4388f, 0.4448f, 0.4508f, 0.4569f, 0.4630f, 0.4691f, + 0.4752f, 0.4814f, 0.4876f, 0.4938f, 0.5000f, 0.5063f, 0.5126f, + 0.5189f, 0.5252f, 0.5316f, 0.5380f, 0.5444f, 0.5508f, 0.5573f, + 0.5638f, 0.5703f, 0.5768f, 0.5834f, 0.5900f, 0.5966f, 0.6033f, + 0.6099f, 0.6166f, 0.6233f, 0.6301f, 0.6369f, 0.6436f, 0.6505f, + 0.6573f, 0.6642f, 0.6710f, 0.6780f, 0.6849f, 0.6919f, 0.6988f, + 0.7059f, 0.7129f, 0.7199f, 0.7270f, 0.7341f, 0.7413f, 0.7484f, + 0.7556f, 0.7628f, 0.7700f, 0.7773f, 0.7846f, 0.7919f, 0.7992f, + 0.8066f, 0.8140f, 0.8214f, 0.8288f, 0.8363f, 0.8437f, 0.8513f, + 0.8588f, 0.8664f, 0.8740f, 0.8816f, 0.8892f, 0.8969f, 0.9046f, + 0.9124f, 0.9201f, 0.9280f, 0.9358f, 0.9437f, 0.9516f, 0.9595f, + 0.9675f, 0.9755f, 0.9836f, 0.9918f, 1.0000f + }; + + public FastOutLinearInInterpolator() { + super(VALUES); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java new file mode 100644 index 00000000000..a21d1310e67 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java @@ -0,0 +1,70 @@ +/* + * 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.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#fast_out_slow_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0.4, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + */ +public class FastOutSlowInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0009f, 0.0014f, 0.0020f, + 0.0027f, 0.0036f, 0.0046f, 0.0058f, 0.0071f, 0.0085f, 0.0101f, + 0.0118f, 0.0137f, 0.0158f, 0.0180f, 0.0205f, 0.0231f, 0.0259f, + 0.0289f, 0.0321f, 0.0355f, 0.0391f, 0.0430f, 0.0471f, 0.0514f, + 0.0560f, 0.0608f, 0.0660f, 0.0714f, 0.0771f, 0.0830f, 0.0893f, + 0.0959f, 0.1029f, 0.1101f, 0.1177f, 0.1257f, 0.1339f, 0.1426f, + 0.1516f, 0.1610f, 0.1707f, 0.1808f, 0.1913f, 0.2021f, 0.2133f, + 0.2248f, 0.2366f, 0.2487f, 0.2611f, 0.2738f, 0.2867f, 0.2998f, + 0.3131f, 0.3265f, 0.3400f, 0.3536f, 0.3673f, 0.3810f, 0.3946f, + 0.4082f, 0.4217f, 0.4352f, 0.4485f, 0.4616f, 0.4746f, 0.4874f, + 0.5000f, 0.5124f, 0.5246f, 0.5365f, 0.5482f, 0.5597f, 0.5710f, + 0.5820f, 0.5928f, 0.6033f, 0.6136f, 0.6237f, 0.6335f, 0.6431f, + 0.6525f, 0.6616f, 0.6706f, 0.6793f, 0.6878f, 0.6961f, 0.7043f, + 0.7122f, 0.7199f, 0.7275f, 0.7349f, 0.7421f, 0.7491f, 0.7559f, + 0.7626f, 0.7692f, 0.7756f, 0.7818f, 0.7879f, 0.7938f, 0.7996f, + 0.8053f, 0.8108f, 0.8162f, 0.8215f, 0.8266f, 0.8317f, 0.8366f, + 0.8414f, 0.8461f, 0.8507f, 0.8551f, 0.8595f, 0.8638f, 0.8679f, + 0.8720f, 0.8760f, 0.8798f, 0.8836f, 0.8873f, 0.8909f, 0.8945f, + 0.8979f, 0.9013f, 0.9046f, 0.9078f, 0.9109f, 0.9139f, 0.9169f, + 0.9198f, 0.9227f, 0.9254f, 0.9281f, 0.9307f, 0.9333f, 0.9358f, + 0.9382f, 0.9406f, 0.9429f, 0.9452f, 0.9474f, 0.9495f, 0.9516f, + 0.9536f, 0.9556f, 0.9575f, 0.9594f, 0.9612f, 0.9629f, 0.9646f, + 0.9663f, 0.9679f, 0.9695f, 0.9710f, 0.9725f, 0.9739f, 0.9753f, + 0.9766f, 0.9779f, 0.9791f, 0.9803f, 0.9815f, 0.9826f, 0.9837f, + 0.9848f, 0.9858f, 0.9867f, 0.9877f, 0.9885f, 0.9894f, 0.9902f, + 0.9910f, 0.9917f, 0.9924f, 0.9931f, 0.9937f, 0.9944f, 0.9949f, + 0.9955f, 0.9960f, 0.9964f, 0.9969f, 0.9973f, 0.9977f, 0.9980f, + 0.9984f, 0.9986f, 0.9989f, 0.9991f, 0.9993f, 0.9995f, 0.9997f, + 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f + }; + + public FastOutSlowInInterpolator() { + super(VALUES); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java new file mode 100644 index 00000000000..41f4cd6a52f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java @@ -0,0 +1,70 @@ +/* + * 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.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#linear_out_slow_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + */ +public class LinearOutSlowInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0222f, 0.0424f, 0.0613f, 0.0793f, 0.0966f, 0.1132f, + 0.1293f, 0.1449f, 0.1600f, 0.1747f, 0.1890f, 0.2029f, 0.2165f, + 0.2298f, 0.2428f, 0.2555f, 0.2680f, 0.2802f, 0.2921f, 0.3038f, + 0.3153f, 0.3266f, 0.3377f, 0.3486f, 0.3592f, 0.3697f, 0.3801f, + 0.3902f, 0.4002f, 0.4100f, 0.4196f, 0.4291f, 0.4385f, 0.4477f, + 0.4567f, 0.4656f, 0.4744f, 0.4831f, 0.4916f, 0.5000f, 0.5083f, + 0.5164f, 0.5245f, 0.5324f, 0.5402f, 0.5479f, 0.5555f, 0.5629f, + 0.5703f, 0.5776f, 0.5847f, 0.5918f, 0.5988f, 0.6057f, 0.6124f, + 0.6191f, 0.6257f, 0.6322f, 0.6387f, 0.6450f, 0.6512f, 0.6574f, + 0.6635f, 0.6695f, 0.6754f, 0.6812f, 0.6870f, 0.6927f, 0.6983f, + 0.7038f, 0.7093f, 0.7147f, 0.7200f, 0.7252f, 0.7304f, 0.7355f, + 0.7406f, 0.7455f, 0.7504f, 0.7553f, 0.7600f, 0.7647f, 0.7694f, + 0.7740f, 0.7785f, 0.7829f, 0.7873f, 0.7917f, 0.7959f, 0.8002f, + 0.8043f, 0.8084f, 0.8125f, 0.8165f, 0.8204f, 0.8243f, 0.8281f, + 0.8319f, 0.8356f, 0.8392f, 0.8429f, 0.8464f, 0.8499f, 0.8534f, + 0.8568f, 0.8601f, 0.8634f, 0.8667f, 0.8699f, 0.8731f, 0.8762f, + 0.8792f, 0.8823f, 0.8852f, 0.8882f, 0.8910f, 0.8939f, 0.8967f, + 0.8994f, 0.9021f, 0.9048f, 0.9074f, 0.9100f, 0.9125f, 0.9150f, + 0.9174f, 0.9198f, 0.9222f, 0.9245f, 0.9268f, 0.9290f, 0.9312f, + 0.9334f, 0.9355f, 0.9376f, 0.9396f, 0.9416f, 0.9436f, 0.9455f, + 0.9474f, 0.9492f, 0.9510f, 0.9528f, 0.9545f, 0.9562f, 0.9579f, + 0.9595f, 0.9611f, 0.9627f, 0.9642f, 0.9657f, 0.9672f, 0.9686f, + 0.9700f, 0.9713f, 0.9726f, 0.9739f, 0.9752f, 0.9764f, 0.9776f, + 0.9787f, 0.9798f, 0.9809f, 0.9820f, 0.9830f, 0.9840f, 0.9849f, + 0.9859f, 0.9868f, 0.9876f, 0.9885f, 0.9893f, 0.9900f, 0.9908f, + 0.9915f, 0.9922f, 0.9928f, 0.9934f, 0.9940f, 0.9946f, 0.9951f, + 0.9956f, 0.9961f, 0.9966f, 0.9970f, 0.9974f, 0.9977f, 0.9981f, + 0.9984f, 0.9987f, 0.9989f, 0.9992f, 0.9994f, 0.9995f, 0.9997f, + 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f + }; + + public LinearOutSlowInInterpolator() { + super(VALUES); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java new file mode 100644 index 00000000000..c2341771303 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java @@ -0,0 +1,57 @@ +/* + * 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.v4.view.animation; + +import android.view.animation.Interpolator; + +/** + * An {@link Interpolator} that uses a lookup table to compute an interpolation based on a + * given input. + */ +abstract class LookupTableInterpolator implements Interpolator { + + private final float[] mValues; + private final float mStepSize; + + public LookupTableInterpolator(float[] values) { + mValues = values; + mStepSize = 1f / (mValues.length - 1); + } + + @Override + public float getInterpolation(float input) { + if (input >= 1.0f) { + return 1.0f; + } + if (input <= 0f) { + return 0f; + } + + // Calculate index - We use min with length - 2 to avoid IndexOutOfBoundsException when + // we lerp (linearly interpolate) in the return statement + int position = Math.min((int) (input * (mValues.length - 1)), mValues.length - 2); + + // Calculate values to account for small offsets as the lookup table has discrete values + float quantized = position * mStepSize; + float diff = input - quantized; + float weight = diff / mStepSize; + + // Linearly interpolate between the table values + return mValues[position] + weight * (mValues[position + 1] - mValues[position]); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java new file mode 100644 index 00000000000..2e06132b3dd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java @@ -0,0 +1,85 @@ +/* + * 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.v4.view.animation; + +import android.graphics.Path; +import android.os.Build; +import android.view.animation.Interpolator; + +/** + * Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the + * platform implementation will be used and on older platforms a compatible alternative + * implementation will be used. + */ +public class PathInterpolatorCompat { + + private PathInterpolatorCompat() { + // prevent instantiation + } + + /** + * Create an {@link Interpolator} for an arbitrary {@link Path}. The {@link Path} + * must begin at {@code (0, 0)} and end at {@code (1, 1)}. The x-coordinate along the + * {@link Path} is the input value and the output is the y coordinate of the line at that + * point. This means that the Path must conform to a function {@code y = f(x)}. + *

    + * The {@link Path} must not have gaps in the x direction and must not + * loop back on itself such that there can be two points sharing the same x coordinate. + * + * @param path the {@link Path} to use to make the line representing the {@link Interpolator} + * @return the {@link Interpolator} representing the {@link Path} + */ + public static Interpolator create(Path path) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(path); + } + return PathInterpolatorCompatBase.create(path); + } + + /** + * Create an {@link Interpolator} for a quadratic Bezier curve. The end points + * {@code (0, 0)} and {@code (1, 1)} are assumed. + * + * @param controlX the x coordinate of the quadratic Bezier control point + * @param controlY the y coordinate of the quadratic Bezier control point + * @return the {@link Interpolator} representing the quadratic Bezier curve + */ + public static Interpolator create(float controlX, float controlY) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(controlX, controlY); + } + return PathInterpolatorCompatBase.create(controlX, controlY); + } + + /** + * Create an {@link Interpolator} for a cubic Bezier curve. The end points + * {@code (0, 0)} and {@code (1, 1)} are assumed. + * + * @param controlX1 the x coordinate of the first control point of the cubic Bezier + * @param controlY1 the y coordinate of the first control point of the cubic Bezier + * @param controlX2 the x coordinate of the second control point of the cubic Bezier + * @param controlY2 the y coordinate of the second control point of the cubic Bezier + * @return the {@link Interpolator} representing the cubic Bezier curve + */ + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(controlX1, controlY1, controlX2, controlY2); + } + return PathInterpolatorCompatBase.create(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java new file mode 100644 index 00000000000..3767f2734d9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java @@ -0,0 +1,44 @@ +/* + * 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.v4.view.animation; + +import android.graphics.Path; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * API 21+ implementation for path interpolator compatibility. + */ +class PathInterpolatorCompatApi21 { + + private PathInterpolatorCompatApi21() { + // prevent instantiation + } + + public static Interpolator create(Path path) { + return new PathInterpolator(path); + } + + public static Interpolator create(float controlX, float controlY) { + return new PathInterpolator(controlX, controlY); + } + + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + return new PathInterpolator(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java new file mode 100644 index 00000000000..9bd4c94d74d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java @@ -0,0 +1,43 @@ +/* + * 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.v4.view.animation; + +import android.graphics.Path; +import android.view.animation.Interpolator; + +/** + * Base implementation for path interpolator compatibility. + */ +class PathInterpolatorCompatBase { + + private PathInterpolatorCompatBase() { + // prevent instantiation + } + + public static Interpolator create(Path path) { + return new PathInterpolatorDonut(path); + } + + public static Interpolator create(float controlX, float controlY) { + return new PathInterpolatorDonut(controlX, controlY); + } + + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + return new PathInterpolatorDonut(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java new file mode 100644 index 00000000000..1240e1f0081 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java @@ -0,0 +1,112 @@ +/* + * 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.v4.view.animation; + +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.view.animation.Interpolator; + +/** + * A path interpolator implementation compatible with API 4+. + */ +class PathInterpolatorDonut implements Interpolator { + + /** + * Governs the accuracy of the approximation of the {@link Path}. + */ + private static final float PRECISION = 0.002f; + + private final float[] mX; + private final float[] mY; + + public PathInterpolatorDonut(Path path) { + final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */); + + final float pathLength = pathMeasure.getLength(); + final int numPoints = (int) (pathLength / PRECISION) + 1; + + mX = new float[numPoints]; + mY = new float[numPoints]; + + final float[] position = new float[2]; + for (int i = 0; i < numPoints; ++i) { + final float distance = (i * pathLength) / (numPoints - 1); + pathMeasure.getPosTan(distance, position, null /* tangent */); + + mX[i] = position[0]; + mY[i] = position[1]; + } + } + + public PathInterpolatorDonut(float controlX, float controlY) { + this(createQuad(controlX, controlY)); + } + + public PathInterpolatorDonut(float controlX1, float controlY1, + float controlX2, float controlY2) { + this(createCubic(controlX1, controlY1, controlX2, controlY2)); + } + + @Override + public float getInterpolation(float t) { + if (t <= 0.0f) { + return 0.0f; + } else if (t >= 1.0f) { + return 1.0f; + } + + // Do a binary search for the correct x to interpolate between. + int startIndex = 0; + int endIndex = mX.length - 1; + while (endIndex - startIndex > 1) { + int midIndex = (startIndex + endIndex) / 2; + if (t < mX[midIndex]) { + endIndex = midIndex; + } else { + startIndex = midIndex; + } + } + + final float xRange = mX[endIndex] - mX[startIndex]; + if (xRange == 0) { + return mY[startIndex]; + } + + final float tInRange = t - mX[startIndex]; + final float fraction = tInRange / xRange; + + final float startY = mY[startIndex]; + final float endY = mY[endIndex]; + + return startY + (fraction * (endY - startY)); + } + + private static Path createQuad(float controlX, float controlY) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.quadTo(controlX, controlY, 1.0f, 1.0f); + return path; + } + + private static Path createCubic(float controlX1, float controlY1, + float controlX2, float controlY2) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0f, 1.0f); + return path; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/package.html b/mobile/google-android-support-v4/src/android/support/v4/view/package.html new file mode 100644 index 00000000000..d80ef708b8a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/package.html @@ -0,0 +1,11 @@ + + +Support android.util classes to assist with development of applications for +android API level 4 or later. The main features here are a variety of classes +for handling backwards compatibility with views (for example +{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch +data if available), and a new +{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over +to the widget package). + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java new file mode 100644 index 00000000000..32e2d45e10f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java @@ -0,0 +1,867 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.res.Resources; +import android.os.SystemClock; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +/** + * AutoScrollHelper is a utility class for adding automatic edge-triggered + * scrolling to Views. + *

    + * Note: Implementing classes are responsible for overriding the + * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and + * {@link #canTargetScrollVertically} methods. See + * {@link ListViewAutoScrollHelper} for a {@link android.widget.ListView} + * -specific implementation. + *

    + *

    Activation

    Automatic scrolling starts when the user touches within + * an activation area. By default, activation areas are defined as the top, + * left, right, and bottom 20% of the host view's total area. Touching within + * the top activation area scrolls up, left scrolls to the left, and so on. + *

    + * As the user touches closer to the extreme edge of the activation area, + * scrolling accelerates up to a maximum velocity. When using the default edge + * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds + * will scroll at the maximum velocity. + *

    + * The following activation properties may be configured: + *

      + *
    • Delay after entering activation area before auto-scrolling begins, see + * {@link #setActivationDelay}. Default value is + * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps. + *
    • Location of activation areas, see {@link #setEdgeType}. Default value is + * {@link #EDGE_TYPE_INSIDE_EXTEND}. + *
    • Size of activation areas relative to view size, see + * {@link #setRelativeEdges}. Default value is 20% for both vertical and + * horizontal edges. + *
    • Maximum size used to constrain relative size, see + * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. + *
    + *

    Scrolling

    When automatic scrolling is active, the helper will + * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets. + *

    + * The following scrolling properties may be configured: + *

      + *
    • Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default + * value is 500 milliseconds. + *
    • Acceleration ramp-down duration, see {@link #setRampDownDuration}. + * Default value is 500 milliseconds. + *
    • Target velocity relative to view size, see {@link #setRelativeVelocity}. + * Default value is 100% per second for both vertical and horizontal. + *
    • Minimum velocity used to constrain relative velocity, see + * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the + * larger of either this value or the relative target value. Default value is + * approximately 5 centimeters or 315 dips per second. + *
    • Maximum velocity used to constrain relative velocity, see + * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or + * 1575 dips per second. + *
    + */ +public abstract class AutoScrollHelper implements View.OnTouchListener { + /** + * Constant passed to {@link #setRelativeEdges} or + * {@link #setRelativeVelocity}. Using this value ensures that the computed + * relative value is ignored and the absolute maximum value is always used. + */ + public static final float RELATIVE_UNSPECIFIED = 0; + + /** + * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity}, + * or {@link #setMinimumVelocity}. Using this value ensures that the + * computed relative value is always used without constraining to a + * particular minimum or maximum value. + */ + public static final float NO_MAX = Float.MAX_VALUE; + + /** + * Constant passed to {@link #setMaximumEdges}, or + * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this + * value ensures that the computed relative value is always used without + * constraining to a particular minimum or maximum value. + */ + public static final float NO_MIN = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. Moving outside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. After activation begins, moving outside the view + * bounds will continue scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE_EXTEND = 1; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending outward. Moving inside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_OUTSIDE = 2; + + private static final int HORIZONTAL = 0; + private static final int VERTICAL = 1; + + /** Scroller used to control acceleration toward maximum velocity. */ + private final ClampedScroller mScroller = new ClampedScroller(); + + /** Interpolator used to scale velocity with touch position. */ + private final Interpolator mEdgeInterpolator = new AccelerateInterpolator(); + + /** The view to auto-scroll. Might not be the source of touch events. */ + private final View mTarget; + + /** Runnable used to animate scrolling. */ + private Runnable mRunnable; + + /** Edge insets used to activate auto-scrolling. */ + private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values for edge insets used to activate auto-scrolling. */ + private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX }; + + /** The type of edge being used. */ + private int mEdgeType; + + /** Delay after entering an activation edge before auto-scrolling begins. */ + private int mActivationDelay; + + /** Relative scrolling velocity at maximum edge distance. */ + private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX }; + + /** Whether to start activation immediately. */ + private boolean mAlreadyDelayed; + + /** Whether to reset the scroller start time on the next animation. */ + private boolean mNeedsReset; + + /** Whether to send a cancel motion event to the target view. */ + private boolean mNeedsCancel; + + /** Whether the auto-scroller is actively scrolling. */ + private boolean mAnimating; + + /** Whether the auto-scroller is enabled. */ + private boolean mEnabled; + + /** Whether the auto-scroller consumes events when scrolling. */ + private boolean mExclusive; + + // Default values. + private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND; + private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315; + private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575; + private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX; + private static final float DEFAULT_RELATIVE_EDGE = 0.2f; + private static final float DEFAULT_RELATIVE_VELOCITY = 1f; + private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout(); + private static final int DEFAULT_RAMP_UP_DURATION = 500; + private static final int DEFAULT_RAMP_DOWN_DURATION = 500; + + /** + * Creates a new helper for scrolling the specified target view. + *

    + * The resulting helper may be configured by chaining setter calls and + * should be set as a touch listener on the target view. + *

    + * By default, the helper is disabled and will not respond to touch events + * until it is enabled using {@link #setEnabled}. + * + * @param target The view to automatically scroll. + */ + public AutoScrollHelper(View target) { + mTarget = target; + + final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + setMaximumVelocity(maxVelocity, maxVelocity); + setMinimumVelocity(minVelocity, minVelocity); + + setEdgeType(DEFAULT_EDGE_TYPE); + setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE); + setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE); + setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY); + setActivationDelay(DEFAULT_ACTIVATION_DELAY); + setRampUpDuration(DEFAULT_RAMP_UP_DURATION); + setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION); + } + + /** + * Sets whether the scroll helper is enabled and should respond to touch + * events. + * + * @param enabled Whether the scroll helper is enabled. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEnabled(boolean enabled) { + if (mEnabled && !enabled) { + requestStop(); + } + + mEnabled = enabled; + return this; + } + + /** + * @return True if this helper is enabled and responding to touch events. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Enables or disables exclusive handling of touch events during scrolling. + * By default, exclusive handling is disabled and the target view receives + * all touch events. + *

    + * When enabled, {@link #onTouch} will return true if the helper is + * currently scrolling and false otherwise. + * + * @param exclusive True to exclusively handle touch events during scrolling, + * false to allow the target view to receive all touch events. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setExclusive(boolean exclusive) { + mExclusive = exclusive; + return this; + } + + /** + * Indicates whether the scroll helper handles touch events exclusively + * during scrolling. + * + * @return True if exclusive handling of touch events during scrolling is + * enabled, false otherwise. + * @see #setExclusive(boolean) + */ + public boolean isExclusive() { + return mExclusive; + } + + /** + * Sets the absolute maximum scrolling velocity. + *

    + * If relative velocity is not specified, scrolling will always reach the + * same maximum velocity. If both relative and maximum velocities are + * specified, the maximum velocity will be used to clamp the calculated + * relative velocity. + * + * @param horizontalMax The maximum horizontal scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @param verticalMax The maximum vertical scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { + mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; + mMaximumVelocity[VERTICAL] = verticalMax / 1000f; + return this; + } + + /** + * Sets the absolute minimum scrolling velocity. + *

    + * If both relative and minimum velocities are specified, the minimum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontalMin The minimum horizontal scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @param verticalMin The minimum vertical scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { + mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; + mMinimumVelocity[VERTICAL] = verticalMin / 1000f; + return this; + } + + /** + * Sets the target scrolling velocity relative to the host view's + * dimensions. + *

    + * If both relative and maximum velocities are specified, the maximum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontal The target horizontal velocity as a fraction of the + * host view width per second, or {@link #RELATIVE_UNSPECIFIED} + * to ignore. + * @param vertical The target vertical velocity as a fraction of the host + * view height per second, or {@link #RELATIVE_UNSPECIFIED} to + * ignore. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { + mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; + mRelativeVelocity[VERTICAL] = vertical / 1000f; + return this; + } + + /** + * Sets the activation edge type, one of: + *

      + *
    • {@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + *
    • {@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + *
    • {@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches + * that move outside the bounds of the host view. + *
    + * + * @param type The type of edge to use. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEdgeType(int type) { + mEdgeType = type; + return this; + } + + /** + * Sets the activation edge size relative to the host view's dimensions. + *

    + * If both relative and maximum edges are specified, the maximum edge will + * be used to constrain the calculated relative edge size. + * + * @param horizontal The horizontal edge size as a fraction of the host view + * width, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @param vertical The vertical edge size as a fraction of the host view + * height, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { + mRelativeEdges[HORIZONTAL] = horizontal; + mRelativeEdges[VERTICAL] = vertical; + return this; + } + + /** + * Sets the absolute maximum edge size. + *

    + * If relative edge size is not specified, activation edges will always be + * the maximum edge size. If both relative and maximum edges are specified, + * the maximum edge will be used to constrain the calculated relative edge + * size. + * + * @param horizontalMax The maximum horizontal edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @param verticalMax The maximum vertical edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { + mMaximumEdges[HORIZONTAL] = horizontalMax; + mMaximumEdges[VERTICAL] = verticalMax; + return this; + } + + /** + * Sets the delay after entering an activation edge before activation of + * auto-scrolling. By default, the activation delay is set to + * {@link ViewConfiguration#getTapTimeout()}. + *

    + * Specifying a delay of zero will start auto-scrolling immediately after + * the touch position enters an activation edge. + * + * @param delayMillis The activation delay in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setActivationDelay(int delayMillis) { + mActivationDelay = delayMillis; + return this; + } + + /** + * Sets the amount of time after activation of auto-scrolling that is takes + * to reach target velocity for the current touch position. + *

    + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-up duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampUpDuration(int durationMillis) { + mScroller.setRampUpDuration(durationMillis); + return this; + } + + /** + * Sets the amount of time after de-activation of auto-scrolling that is + * takes to slow to a stop. + *

    + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-down duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampDownDuration(int durationMillis) { + mScroller.setRampDownDuration(durationMillis); + return this; + } + + /** + * Handles touch events by activating automatic scrolling, adjusting scroll + * velocity, or stopping. + *

    + * If {@link #isExclusive()} is false, always returns false so that + * the host view may handle touch events. Otherwise, returns true when + * automatic scrolling is active and false otherwise. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!mEnabled) { + return false; + } + + final int action = MotionEventCompat.getActionMasked(event); + switch (action) { + case MotionEvent.ACTION_DOWN: + mNeedsCancel = true; + mAlreadyDelayed = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final float xTargetVelocity = computeTargetVelocity( + HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth()); + final float yTargetVelocity = computeTargetVelocity( + VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight()); + mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity); + + // If the auto scroller was not previously active, but it should + // be, then update the state and start animations. + if (!mAnimating && shouldAnimate()) { + startAnimating(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + requestStop(); + break; + } + + return mExclusive && mAnimating; + } + + /** + * @return whether the target is able to scroll in the requested direction + */ + private boolean shouldAnimate() { + final ClampedScroller scroller = mScroller; + final int verticalDirection = scroller.getVerticalDirection(); + final int horizontalDirection = scroller.getHorizontalDirection(); + + return verticalDirection != 0 && canTargetScrollVertically(verticalDirection) + || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection); + } + + /** + * Starts the scroll animation. + */ + private void startAnimating() { + if (mRunnable == null) { + mRunnable = new ScrollAnimationRunnable(); + } + + mAnimating = true; + mNeedsReset = true; + + if (!mAlreadyDelayed && mActivationDelay > 0) { + ViewCompat.postOnAnimationDelayed(mTarget, mRunnable, mActivationDelay); + } else { + mRunnable.run(); + } + + // If we start animating again before the user lifts their finger, we + // already know it's not a tap and don't need an activation delay. + mAlreadyDelayed = true; + } + + /** + * Requests that the scroll animation slow to a stop. If there is an + * activation delay, this may occur between posting the animation and + * actually running it. + */ + private void requestStop() { + if (mNeedsReset) { + // The animation has been posted, but hasn't run yet. Manually + // stopping animation will prevent it from running. + mAnimating = false; + } else { + mScroller.requestStop(); + } + } + + private float computeTargetVelocity( + int direction, float coordinate, float srcSize, float dstSize) { + final float relativeEdge = mRelativeEdges[direction]; + final float maximumEdge = mMaximumEdges[direction]; + final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate); + if (value == 0) { + // The edge in this direction is not activated. + return 0; + } + + final float relativeVelocity = mRelativeVelocity[direction]; + final float minimumVelocity = mMinimumVelocity[direction]; + final float maximumVelocity = mMaximumVelocity[direction]; + final float targetVelocity = relativeVelocity * dstSize; + + // Target velocity is adjusted for interpolated edge position, then + // clamped to the minimum and maximum values. Later, this value will be + // adjusted for time-based acceleration. + if (value > 0) { + return constrain(value * targetVelocity, minimumVelocity, maximumVelocity); + } else { + return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity); + } + } + + /** + * Override this method to scroll the target view by the specified number of + * pixels. + * + * @param deltaX The number of pixels to scroll by horizontally. + * @param deltaY The number of pixels to scroll by vertically. + */ + public abstract void scrollTargetBy(int deltaX, int deltaY); + + /** + * Override this method to return whether the target view can be scrolled + * horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check + * scrolling right. + * @return true if the target view is able to horizontally scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollHorizontally(int direction); + + /** + * Override this method to return whether the target view can be scrolled + * vertically in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the target view is able to vertically scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollVertically(int direction); + + /** + * Returns the interpolated position of a touch point relative to an edge + * defined by its relative inset, its maximum absolute inset, and the edge + * interpolator. + * + * @param relativeValue The size of the inset relative to the total size. + * @param size Total size. + * @param maxValue The maximum size of the inset, used to clamp (relative * + * total). + * @param current Touch position within within the total size. + * @return Interpolated value of the touch position within the edge. + */ + private float getEdgeValue(float relativeValue, float size, float maxValue, float current) { + // For now, leading and trailing edges are always the same size. + final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue); + final float valueLeading = constrainEdgeValue(current, edgeSize); + final float valueTrailing = constrainEdgeValue(size - current, edgeSize); + final float value = (valueTrailing - valueLeading); + final float interpolated; + if (value < 0) { + interpolated = -mEdgeInterpolator.getInterpolation(-value); + } else if (value > 0) { + interpolated = mEdgeInterpolator.getInterpolation(value); + } else { + return 0; + } + + return constrain(interpolated, -1, 1); + } + + private float constrainEdgeValue(float current, float leading) { + if (leading == 0) { + return 0; + } + + switch (mEdgeType) { + case EDGE_TYPE_INSIDE: + case EDGE_TYPE_INSIDE_EXTEND: + if (current < leading) { + if (current >= 0) { + // Movement up to the edge is scaled. + return 1f - current / leading; + } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) { + // Movement beyond the edge is always maximum. + return 1f; + } + } + break; + case EDGE_TYPE_OUTSIDE: + if (current < 0) { + // Movement beyond the edge is scaled. + return current / -leading; + } + break; + } + + return 0; + } + + private static int constrain(int value, int min, int max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + private static float constrain(float value, float min, float max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + /** + * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view, + * canceling any ongoing touch events. + */ + private void cancelTargetTouch() { + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent cancel = MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mTarget.onTouchEvent(cancel); + cancel.recycle(); + } + + private class ScrollAnimationRunnable implements Runnable { + @Override + public void run() { + if (!mAnimating) { + return; + } + + if (mNeedsReset) { + mNeedsReset = false; + mScroller.start(); + } + + final ClampedScroller scroller = mScroller; + if (scroller.isFinished() || !shouldAnimate()) { + mAnimating = false; + return; + } + + if (mNeedsCancel) { + mNeedsCancel = false; + cancelTargetTouch(); + } + + scroller.computeScrollDelta(); + + final int deltaX = scroller.getDeltaX(); + final int deltaY = scroller.getDeltaY(); + scrollTargetBy(deltaX, deltaY); + + // Keep going until the scroller has permanently stopped. + ViewCompat.postOnAnimation(mTarget, this); + } + } + + /** + * Scroller whose velocity follows the curve of an {@link Interpolator} and + * is clamped to the interpolated 0f value before starting and the + * interpolated 1f value after a specified duration. + */ + private static class ClampedScroller { + private int mRampUpDuration; + private int mRampDownDuration; + private float mTargetVelocityX; + private float mTargetVelocityY; + + private long mStartTime; + + private long mDeltaTime; + private int mDeltaX; + private int mDeltaY; + + private long mStopTime; + private float mStopValue; + private int mEffectiveRampDown; + + /** + * Creates a new ramp-up scroller that reaches full velocity after a + * specified duration. + */ + public ClampedScroller() { + mStartTime = Long.MIN_VALUE; + mStopTime = -1; + mDeltaTime = 0; + mDeltaX = 0; + mDeltaY = 0; + } + + public void setRampUpDuration(int durationMillis) { + mRampUpDuration = durationMillis; + } + + public void setRampDownDuration(int durationMillis) { + mRampDownDuration = durationMillis; + } + + /** + * Starts the scroller at the current animation time. + */ + public void start() { + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStopTime = -1; + mDeltaTime = mStartTime; + mStopValue = 0.5f; + mDeltaX = 0; + mDeltaY = 0; + } + + /** + * Stops the scroller at the current animation time. + */ + public void requestStop() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration); + mStopValue = getValueAt(currentTime); + mStopTime = currentTime; + } + + public boolean isFinished() { + return mStopTime > 0 + && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown; + } + + private float getValueAt(long currentTime) { + if (currentTime < mStartTime) { + return 0f; + } else if (mStopTime < 0 || currentTime < mStopTime) { + final long elapsedSinceStart = currentTime - mStartTime; + return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1); + } else { + final long elapsedSinceEnd = currentTime - mStopTime; + return (1 - mStopValue) + mStopValue + * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1); + } + } + + /** + * Interpolates the value along a parabolic curve corresponding to the equation + * y = -4x * (x-1). + * + * @param value The value to interpolate, between 0 and 1. + * @return the interpolated value, between 0 and 1. + */ + private float interpolateValue(float value) { + return -4 * value * value + 4 * value; + } + + /** + * Computes the current scroll deltas. This usually only be called after + * starting the scroller with {@link #start()}. + * + * @see #getDeltaX() + * @see #getDeltaY() + */ + public void computeScrollDelta() { + if (mDeltaTime == 0) { + throw new RuntimeException("Cannot compute scroll delta before calling start()"); + } + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + final float value = getValueAt(currentTime); + final float scale = interpolateValue(value); + final long elapsedSinceDelta = currentTime - mDeltaTime; + + mDeltaTime = currentTime; + mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX); + mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY); + } + + /** + * Sets the target velocity for this scroller. + * + * @param x The target X velocity in pixels per millisecond. + * @param y The target Y velocity in pixels per millisecond. + */ + public void setTargetVelocity(float x, float y) { + mTargetVelocityX = x; + mTargetVelocityY = y; + } + + public int getHorizontalDirection() { + return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX)); + } + + public int getVerticalDirection() { + return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY)); + } + + /** + * The distance traveled in the X-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaX() { + return mDeltaX; + } + + /** + * The distance traveled in the Y-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaY() { + return mDeltaY; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java new file mode 100644 index 00000000000..246ffa746f3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java @@ -0,0 +1,153 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Shader; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.support.v4.view.ViewCompat; +import android.view.animation.Animation; +import android.widget.ImageView; + +/** + * Private class created to work around issues with AnimationListeners being + * called before the animation is actually complete and support shadows on older + * platforms. + * + * @hide + */ +class CircleImageView extends ImageView { + + private static final int KEY_SHADOW_COLOR = 0x1E000000; + private static final int FILL_SHADOW_COLOR = 0x3D000000; + // PX + private static final float X_OFFSET = 0f; + private static final float Y_OFFSET = 1.75f; + private static final float SHADOW_RADIUS = 3.5f; + private static final int SHADOW_ELEVATION = 4; + + private Animation.AnimationListener mListener; + private int mShadowRadius; + + public CircleImageView(Context context, int color, final float radius) { + super(context); + final float density = getContext().getResources().getDisplayMetrics().density; + final int diameter = (int) (radius * density * 2); + final int shadowYOffset = (int) (density * Y_OFFSET); + final int shadowXOffset = (int) (density * X_OFFSET); + + mShadowRadius = (int) (density * SHADOW_RADIUS); + + ShapeDrawable circle; + if (elevationSupported()) { + circle = new ShapeDrawable(new OvalShape()); + ViewCompat.setElevation(this, SHADOW_ELEVATION * density); + } else { + OvalShape oval = new OvalShadow(mShadowRadius, diameter); + circle = new ShapeDrawable(oval); + ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); + circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, + KEY_SHADOW_COLOR); + final int padding = mShadowRadius; + // set padding so the inner image sits correctly within the shadow. + setPadding(padding, padding, padding, padding); + } + circle.getPaint().setColor(color); + setBackgroundDrawable(circle); + } + + private boolean elevationSupported() { + return android.os.Build.VERSION.SDK_INT >= 21; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (!elevationSupported()) { + setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight() + + mShadowRadius*2); + } + } + + public void setAnimationListener(Animation.AnimationListener listener) { + mListener = listener; + } + + @Override + public void onAnimationStart() { + super.onAnimationStart(); + if (mListener != null) { + mListener.onAnimationStart(getAnimation()); + } + } + + @Override + public void onAnimationEnd() { + super.onAnimationEnd(); + if (mListener != null) { + mListener.onAnimationEnd(getAnimation()); + } + } + + /** + * Update the background color of the circle image view. + * + * @param colorRes Id of a color resource. + */ + public void setBackgroundColorRes(int colorRes) { + setBackgroundColor(getContext().getResources().getColor(colorRes)); + } + + @Override + public void setBackgroundColor(int color) { + if (getBackground() instanceof ShapeDrawable) { + ((ShapeDrawable) getBackground()).getPaint().setColor(color); + } + } + + private class OvalShadow extends OvalShape { + private RadialGradient mRadialGradient; + private Paint mShadowPaint; + private int mCircleDiameter; + + public OvalShadow(int shadowRadius, int circleDiameter) { + super(); + mShadowPaint = new Paint(); + mShadowRadius = shadowRadius; + mCircleDiameter = circleDiameter; + mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, + mShadowRadius, new int[] { + FILL_SHADOW_COLOR, Color.TRANSPARENT + }, null, Shader.TileMode.CLAMP); + mShadowPaint.setShader(mRadialGradient); + } + + @Override + public void draw(Canvas canvas, Paint paint) { + final int viewWidth = CircleImageView.this.getWidth(); + final int viewHeight = CircleImageView.this.getHeight(); + canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), + mShadowPaint); + canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java new file mode 100644 index 00000000000..3335b9f9be1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java @@ -0,0 +1,128 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ProgressBar; + +/** + * ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be + * dismissed before showing. Once visible, the progress bar will be visible for + * a minimum amount of time to avoid "flashes" in the UI when an event could take + * a largely variable time to complete (from none, to a user perceivable amount) + */ +public class ContentLoadingProgressBar extends ProgressBar { + private static final int MIN_SHOW_TIME = 500; // ms + private static final int MIN_DELAY = 500; // ms + + private long mStartTime = -1; + + private boolean mPostedHide = false; + + private boolean mPostedShow = false; + + private boolean mDismissed = false; + + private final Runnable mDelayedHide = new Runnable() { + + @Override + public void run() { + mPostedHide = false; + mStartTime = -1; + setVisibility(View.GONE); + } + }; + + private final Runnable mDelayedShow = new Runnable() { + + @Override + public void run() { + mPostedShow = false; + if (!mDismissed) { + mStartTime = System.currentTimeMillis(); + setVisibility(View.VISIBLE); + } + } + }; + + public ContentLoadingProgressBar(Context context) { + this(context, null); + } + + public ContentLoadingProgressBar(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + removeCallbacks(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(); + } + + private void removeCallbacks() { + removeCallbacks(mDelayedHide); + removeCallbacks(mDelayedShow); + } + + /** + * Hide the progress view if it is visible. The progress view will not be + * hidden until it has been shown for at least a minimum show time. If the + * progress view was not yet visible, cancels showing the progress view. + */ + public void hide() { + mDismissed = true; + removeCallbacks(mDelayedShow); + long diff = System.currentTimeMillis() - mStartTime; + if (diff >= MIN_SHOW_TIME || mStartTime == -1) { + // The progress spinner has been shown long enough + // OR was not shown yet. If it wasn't shown yet, + // it will just never be shown. + setVisibility(View.GONE); + } else { + // The progress spinner is shown, but not long enough, + // so put a delayed message in to hide it when its been + // shown long enough. + if (!mPostedHide) { + postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); + mPostedHide = true; + } + } + } + + /** + * Show the progress view after waiting for a minimum delay. If + * during that time, hide() is called, the view is never made visible. + */ + public void show() { + // Reset the start time. + mStartTime = -1; + mDismissed = false; + removeCallbacks(mDelayedHide); + if (!mPostedShow) { + postDelayed(mDelayedShow, MIN_DELAY); + mPostedShow = true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java new file mode 100644 index 00000000000..96d9cfa25c0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.FilterQueryProvider; +import android.widget.Filterable; + +/** + * Static library support version of the framework's {@link android.widget.CursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class CursorAdapter extends BaseAdapter implements Filterable, + CursorFilter.CursorFilterClient { + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mDataValid; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mAutoRequery; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Cursor mCursor; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Context mContext; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int mRowIDColumn; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected ChangeObserver mChangeObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected DataSetObserver mDataSetObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected CursorFilter mCursorFilter; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected FilterQueryProvider mFilterQueryProvider; + + /** + * If set the adapter will call requery() on the cursor whenever a content change + * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public static final int FLAG_AUTO_REQUERY = 0x01; + + /** + * If set the adapter will register a content observer on the cursor and will call + * {@link #onContentChanged()} when a notification comes in. Be careful when + * using this flag: you will need to unset the current Cursor from the adapter + * to avoid leaks due to its registered observers. This flag is not needed + * when using a CursorAdapter with a + * {@link android.content.CursorLoader}. + */ + public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; + + /** + * Constructor that always enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + * + * @param c The cursor from which to get the data. + * @param context The context + */ + @Deprecated + public CursorAdapter(Context context, Cursor c) { + init(context, c, FLAG_AUTO_REQUERY); + } + + /** + * Constructor that allows control over auto-requery. It is recommended + * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param c The cursor from which to get the data. + * @param context The context + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public CursorAdapter(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + /** + * Recommended constructor. + * + * @param c The cursor from which to get the data. + * @param context The context + * @param flags Flags used to determine the behavior of the adapter; may + * be any combination of {@link #FLAG_AUTO_REQUERY} and + * {@link #FLAG_REGISTER_CONTENT_OBSERVER}. + */ + public CursorAdapter(Context context, Cursor c, int flags) { + init(context, c, flags); + } + + /** + * @deprecated Don't use this, use the normal constructor. This will + * be removed in the future. + */ + @Deprecated + protected void init(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + void init(Context context, Cursor c, int flags) { + if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { + flags |= FLAG_REGISTER_CONTENT_OBSERVER; + mAutoRequery = true; + } else { + mAutoRequery = false; + } + boolean cursorPresent = c != null; + mCursor = c; + mDataValid = cursorPresent; + mContext = context; + mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; + if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { + mChangeObserver = new ChangeObserver(); + mDataSetObserver = new MyDataSetObserver(); + } else { + mChangeObserver = null; + mDataSetObserver = null; + } + + if (cursorPresent) { + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); + } + } + + /** + * Returns the cursor. + * @return the cursor. + */ + public Cursor getCursor() { + return mCursor; + } + + /** + * @see android.widget.ListAdapter#getCount() + */ + public int getCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + /** + * @see android.widget.ListAdapter#getItem(int) + */ + public Object getItem(int position) { + if (mDataValid && mCursor != null) { + mCursor.moveToPosition(position); + return mCursor; + } else { + return null; + } + } + + /** + * @see android.widget.ListAdapter#getItemId(int) + */ + public long getItemId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + @Override + public boolean hasStableIds() { + return true; + } + + /** + * @see android.widget.ListAdapter#getView(int, View, ViewGroup) + */ + public View getView(int position, View convertView, ViewGroup parent) { + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + View v; + if (convertView == null) { + v = newView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (mDataValid) { + mCursor.moveToPosition(position); + View v; + if (convertView == null) { + v = newDropDownView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } else { + return null; + } + } + + /** + * Makes a new view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public abstract View newView(Context context, Cursor cursor, ViewGroup parent); + + /** + * Makes a new drop down view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return newView(context, cursor, parent); + } + + /** + * Bind an existing view to the data pointed to by cursor + * @param view Existing view, returned earlier by newView + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + */ + public abstract void bindView(View view, Context context, Cursor cursor); + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + * + * @param cursor The new cursor to be used + */ + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(Cursor)}, the returned old Cursor is not + * closed. + * + * @param newCursor The new cursor to be used. + * @return Returns the previously set Cursor, or null if there wasa not one. + * If the given new Cursor is the same instance is the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + if (oldCursor != null) { + if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); + if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = newCursor; + if (newCursor != null) { + if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); + } + return oldCursor; + } + + /** + *

    Converts the cursor into a CharSequence. Subclasses should override this + * method to convert their results. The default implementation returns an + * empty String for null values or the default String representation of + * the value.

    + * + * @param cursor the cursor to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + * + * The query is provided by a + * {@link android.widget.FilterQueryProvider}. + * If no provider is specified, the current cursor is not filtered and returned. + * + * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} + * and the previous cursor is closed. + * + * This method is always executed on a background thread, not on the + * application's main thread (or UI thread.) + * + * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must be filtered + * + * @return a Cursor representing the results of the new query + * + * @see #getFilter() + * @see #getFilterQueryProvider() + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mCursor; + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * Returns the query filter provider used for filtering. When the + * provider is null, no filtering occurs. + * + * @return the current filter query provider or null if it does not exist + * + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * Sets the query filter provider used to filter the current Cursor. + * The provider's + * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} + * method is invoked when filtering is requested by a client of + * this adapter. + * + * @param filterQueryProvider the filter query provider or null to remove it + * + * @see #getFilterQueryProvider() + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + /** + * Called when the {@link ContentObserver} on the cursor receives a change notification. + * The default implementation provides the auto-requery logic, but may be overridden by + * sub classes. + * + * @see ContentObserver#onChange(boolean) + */ + protected void onContentChanged() { + if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + mDataValid = mCursor.requery(); + } + } + + private class ChangeObserver extends ContentObserver { + public ChangeObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java new file mode 100644 index 00000000000..19fcead2436 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.database.Cursor; +import android.widget.Filter; + +/** + * The CursorFilter delegates most of the work to the + * {@link android.widget.CursorAdapter}. Subclasses should override these + * delegate methods to run the queries and convert the results into String + * that can be used by auto-completion widgets. + */ +class CursorFilter extends Filter { + + CursorFilterClient mClient; + + interface CursorFilterClient { + CharSequence convertToString(Cursor cursor); + Cursor runQueryOnBackgroundThread(CharSequence constraint); + Cursor getCursor(); + void changeCursor(Cursor cursor); + } + + CursorFilter(CursorFilterClient client) { + mClient = client; + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + return mClient.convertToString((Cursor) resultValue); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); + + FilterResults results = new FilterResults(); + if (cursor != null) { + results.count = cursor.getCount(); + results.values = cursor; + } else { + results.count = 0; + results.values = null; + } + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + Cursor oldCursor = mClient.getCursor(); + + if (results.values != null && results.values != oldCursor) { + mClient.changeCursor((Cursor) results.values); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java new file mode 100644 index 00000000000..a8967a273c7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java @@ -0,0 +1,1958 @@ +/* + * 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. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.KeyEventCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewGroupCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * DrawerLayout acts as a top-level container for window content that allows for + * interactive "drawer" views to be pulled out from the edge of the window. + * + *

    Drawer positioning and layout is controlled using the android:layout_gravity + * attribute on child views corresponding to which side of the view you want the drawer + * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) + *

    + * + *

    To use a DrawerLayout, position your primary content view as the first child with + * a width and height of match_parent. Add drawers as child views after the main + * content view and set the layout_gravity appropriately. Drawers commonly use + * match_parent for height with a fixed width.

    + * + *

    {@link DrawerListener} can be used to monitor the state and motion of drawer views. + * Avoid performing expensive operations such as layout during animation as it can cause + * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. + * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.

    + * + *

    As per the Android Design + * guide, any drawers positioned to the left/start should + * always contain content for navigating around the application, whereas any drawers + * positioned to the right/end should always contain actions to take on the current content. + * This preserves the same navigation left, actions right structure present in the Action Bar + * and elsewhere.

    + * + *

    For more information about how to use DrawerLayout, read Creating a Navigation + * Drawer.

    + */ +public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl { + private static final String TAG = "DrawerLayout"; + + @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) + @Retention(RetentionPolicy.SOURCE) + private @interface State {} + + /** + * Indicates that any drawers are in an idle, settled state. No animation is in progress. + */ + public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; + + /** + * Indicates that a drawer is currently being dragged by the user. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * Indicates that a drawer is in the process of settling to a final position. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** @hide */ + @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN}) + @Retention(RetentionPolicy.SOURCE) + private @interface LockMode {} + + /** + * The drawer is unlocked. + */ + public static final int LOCK_MODE_UNLOCKED = 0; + + /** + * The drawer is locked closed. The user may not open it, though + * the app may open it programmatically. + */ + public static final int LOCK_MODE_LOCKED_CLOSED = 1; + + /** + * The drawer is locked open. The user may not close it, though the app + * may close it programmatically. + */ + public static final int LOCK_MODE_LOCKED_OPEN = 2; + + /** @hide */ + @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END}) + @Retention(RetentionPolicy.SOURCE) + private @interface EdgeGravity {} + + + private static final int MIN_DRAWER_MARGIN = 64; // dp + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + /** + * Length of time to delay before peeking the drawer. + */ + private static final int PEEK_DELAY = 160; // ms + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * Experimental feature. + */ + private static final boolean ALLOW_EDGE_LOCK = false; + + private static final boolean CHILDREN_DISALLOW_INTERCEPT = true; + + private static final float TOUCH_SLOP_SENSITIVITY = 1.f; + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** Whether we can use NO_HIDE_DESCENDANTS accessibility importance. */ + private static final boolean CAN_HIDE_DESCENDANTS = Build.VERSION.SDK_INT >= 19; + + private final ChildAccessibilityDelegate mChildAccessibilityDelegate = + new ChildAccessibilityDelegate(); + + private int mMinDrawerMargin; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + private float mScrimOpacity; + private Paint mScrimPaint = new Paint(); + + private final ViewDragHelper mLeftDragger; + private final ViewDragHelper mRightDragger; + private final ViewDragCallback mLeftCallback; + private final ViewDragCallback mRightCallback; + private int mDrawerState; + private boolean mInLayout; + private boolean mFirstLayout = true; + private int mLockModeLeft; + private int mLockModeRight; + private boolean mDisallowInterceptRequested; + private boolean mChildrenCanceledTouch; + + private DrawerListener mListener; + + private float mInitialMotionX; + private float mInitialMotionY; + + private Drawable mShadowLeft; + private Drawable mShadowRight; + private Drawable mStatusBarBackground; + + private CharSequence mTitleLeft; + private CharSequence mTitleRight; + + private Object mLastInsets; + private boolean mDrawStatusBarBackground; + + /** + * Listener for monitoring events about drawers. + */ + public interface DrawerListener { + /** + * Called when a drawer's position changes. + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + public void onDrawerSlide(View drawerView, float slideOffset); + + /** + * Called when a drawer has settled in a completely open state. + * The drawer is interactive at this point. + * + * @param drawerView Drawer view that is now open + */ + public void onDrawerOpened(View drawerView); + + /** + * Called when a drawer has settled in a completely closed state. + * + * @param drawerView Drawer view that is now closed + */ + public void onDrawerClosed(View drawerView); + + /** + * Called when the drawer motion state changes. The new state will + * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @param newState The new drawer motion state + */ + public void onDrawerStateChanged(@State int newState); + } + + /** + * Stub/no-op implementations of all methods of {@link DrawerListener}. + * Override this if you only care about a few of the available callback methods. + */ + public static abstract class SimpleDrawerListener implements DrawerListener { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + public void onDrawerOpened(View drawerView) { + } + + @Override + public void onDrawerClosed(View drawerView) { + } + + @Override + public void onDrawerStateChanged(int newState) { + } + } + + interface DrawerLayoutCompatImpl { + void configureApplyInsets(View drawerLayout); + void dispatchChildInsets(View child, Object insets, int drawerGravity); + void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity); + int getTopInset(Object lastInsets); + Drawable getDefaultStatusBarBackground(Context context); + } + + static class DrawerLayoutCompatImplBase implements DrawerLayoutCompatImpl { + public void configureApplyInsets(View drawerLayout) { + // This space for rent + } + + public void dispatchChildInsets(View child, Object insets, int drawerGravity) { + // This space for rent + } + + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { + // This space for rent + } + + public int getTopInset(Object insets) { + return 0; + } + + @Override + public Drawable getDefaultStatusBarBackground(Context context) { + return null; + } + } + + static class DrawerLayoutCompatImplApi21 implements DrawerLayoutCompatImpl { + public void configureApplyInsets(View drawerLayout) { + DrawerLayoutCompatApi21.configureApplyInsets(drawerLayout); + } + + public void dispatchChildInsets(View child, Object insets, int drawerGravity) { + DrawerLayoutCompatApi21.dispatchChildInsets(child, insets, drawerGravity); + } + + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { + DrawerLayoutCompatApi21.applyMarginInsets(lp, insets, drawerGravity); + } + + public int getTopInset(Object insets) { + return DrawerLayoutCompatApi21.getTopInset(insets); + } + + @Override + public Drawable getDefaultStatusBarBackground(Context context) { + return DrawerLayoutCompatApi21.getDefaultStatusBarBackground(context); + } + } + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new DrawerLayoutCompatImplApi21(); + } else { + IMPL = new DrawerLayoutCompatImplBase(); + } + } + + static final DrawerLayoutCompatImpl IMPL; + + public DrawerLayout(Context context) { + this(context, null); + } + + public DrawerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + final float density = getResources().getDisplayMetrics().density; + mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); + final float minVel = MIN_FLING_VELOCITY * density; + + mLeftCallback = new ViewDragCallback(Gravity.LEFT); + mRightCallback = new ViewDragCallback(Gravity.RIGHT); + + mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); + mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + mLeftDragger.setMinVelocity(minVel); + mLeftCallback.setDragger(mLeftDragger); + + mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); + mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + mRightDragger.setMinVelocity(minVel); + mRightCallback.setDragger(mRightDragger); + + // So that we can catch the back button + setFocusableInTouchMode(true); + + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); + ViewGroupCompat.setMotionEventSplittingEnabled(this, false); + if (ViewCompat.getFitsSystemWindows(this)) { + IMPL.configureApplyInsets(this); + mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); + } + } + + /** + * @hide Internal use only; called to apply window insets when configured + * with fitsSystemWindows="true" + */ + @Override + public void setChildInsets(Object insets, boolean draw) { + mLastInsets = insets; + mDrawStatusBarBackground = draw; + setWillNotDraw(!draw && getBackground() == null); + requestLayout(); + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param shadowDrawable Shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(Drawable shadowDrawable, @EdgeGravity int gravity) { + /* + * TODO Someone someday might want to set more complex drawables here. + * They're probably nuts, but we might want to consider registering callbacks, + * setting states, etc. properly. + */ + + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(this)); + if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { + mShadowLeft = shadowDrawable; + invalidate(); + } + if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { + mShadowRight = shadowDrawable; + invalidate(); + } + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param resId Resource id of a shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(@DrawableRes int resId, @EdgeGravity int gravity) { + setDrawerShadow(getResources().getDrawable(resId), gravity); + } + + /** + * Set a color to use for the scrim that obscures primary content while a drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set a listener to be notified of drawer events. + * + * @param listener Listener to notify when drawer events occur + * @see DrawerListener + */ + public void setDrawerListener(DrawerListener listener) { + mListener = listener; + } + + /** + * Enable or disable interaction with all drawers. + * + *

    This allows the application to restrict the user's ability to open or close + * any drawer within this layout. DrawerLayout will still respond to calls to + * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking drawers open or closed will implicitly open or close + * any drawers as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public void setDrawerLockMode(@LockMode int lockMode) { + setDrawerLockMode(lockMode, Gravity.LEFT); + setDrawerLockMode(lockMode, Gravity.RIGHT); + } + + /** + * Enable or disable interaction with the given drawer. + * + *

    This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. + * Expresses which drawer to change the mode for. + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, + ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + mLockModeLeft = lockMode; + } else if (absGravity == Gravity.RIGHT) { + mLockModeRight = lockMode; + } + if (lockMode != LOCK_MODE_UNLOCKED) { + // Cancel interaction in progress + final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; + helper.cancel(); + } + switch (lockMode) { + case LOCK_MODE_LOCKED_OPEN: + final View toOpen = findDrawerWithGravity(absGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + break; + case LOCK_MODE_LOCKED_CLOSED: + final View toClose = findDrawerWithGravity(absGravity); + if (toClose != null) { + closeDrawer(toClose); + } + break; + // default: do nothing + } + } + + /** + * Enable or disable interaction with the given drawer. + * + *

    This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param drawerView The drawer view to change the lock mode for + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(@LockMode int lockMode, View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a " + + "drawer with appropriate layout_gravity"); + } + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + setDrawerLockMode(lockMode, gravity); + } + + /** + * Check the lock mode of the drawer with the given gravity. + * + * @param edgeGravity Gravity of the drawer to check + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + @LockMode + public int getDrawerLockMode(@EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Check the lock mode of the given drawer view. + * + * @param drawerView Drawer view to check lock mode + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + @LockMode + public int getDrawerLockMode(View drawerView) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Sets the title of the drawer with the given gravity. + *

    + * When accessibility is turned on, this is the title that will be used to + * identify the drawer to the active accessibility service. + * + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which + * drawer to set the title for. + * @param title The title for the drawer. + */ + public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + mTitleLeft = title; + } else if (absGravity == Gravity.RIGHT) { + mTitleRight = title; + } + } + + /** + * Returns the title of the drawer with the given gravity. + * + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which + * drawer to return the title for. + * @return The title of the drawer, or null if none set. + * @see #setDrawerTitle(int, CharSequence) + */ + @Nullable + public CharSequence getDrawerTitle(@EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + return mTitleLeft; + } else if (absGravity == Gravity.RIGHT) { + return mTitleRight; + } + return null; + } + + /** + * Resolve the shared state of all drawers from the component ViewDragHelpers. + * Should be called whenever a ViewDragHelper's state changes. + */ + void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) { + final int leftState = mLeftDragger.getViewDragState(); + final int rightState = mRightDragger.getViewDragState(); + + final int state; + if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { + state = STATE_DRAGGING; + } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { + state = STATE_SETTLING; + } else { + state = STATE_IDLE; + } + + if (activeDrawer != null && activeState == STATE_IDLE) { + final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); + if (lp.onScreen == 0) { + dispatchOnDrawerClosed(activeDrawer); + } else if (lp.onScreen == 1) { + dispatchOnDrawerOpened(activeDrawer); + } + } + + if (state != mDrawerState) { + mDrawerState = state; + + if (mListener != null) { + mListener.onDrawerStateChanged(state); + } + } + } + + void dispatchOnDrawerClosed(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (lp.knownOpen) { + lp.knownOpen = false; + if (mListener != null) { + mListener.onDrawerClosed(drawerView); + } + + updateChildrenImportantForAccessibility(drawerView, false); + + // Only send WINDOW_STATE_CHANGE if the host has window focus. This + // may change if support for multiple foreground windows (e.g. IME) + // improves. + if (hasWindowFocus()) { + final View rootView = getRootView(); + if (rootView != null) { + rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + } + } + + void dispatchOnDrawerOpened(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (!lp.knownOpen) { + lp.knownOpen = true; + if (mListener != null) { + mListener.onDrawerOpened(drawerView); + } + + updateChildrenImportantForAccessibility(drawerView, true); + + // Only send WINDOW_STATE_CHANGE if the host has window focus. + if (hasWindowFocus()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + drawerView.requestFocus(); + } + } + + private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!isDrawerOpen && !isDrawerView(child) + || isDrawerOpen && child == drawerView) { + // Drawer is closed and this is a content view or this is an + // open drawer view, so it should be visible. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } else { + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + } + } + + void dispatchOnDrawerSlide(View drawerView, float slideOffset) { + if (mListener != null) { + mListener.onDrawerSlide(drawerView, slideOffset); + } + } + + void setDrawerViewOffset(View drawerView, float slideOffset) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (slideOffset == lp.onScreen) { + return; + } + + lp.onScreen = slideOffset; + dispatchOnDrawerSlide(drawerView, slideOffset); + } + + float getDrawerViewOffset(View drawerView) { + return ((LayoutParams) drawerView.getLayoutParams()).onScreen; + } + + /** + * @return the absolute gravity of the child drawerView, resolved according + * to the current layout direction + */ + int getDrawerViewAbsoluteGravity(View drawerView) { + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); + } + + boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + return (absGravity & checkFor) == checkFor; + } + + View findOpenDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (((LayoutParams) child.getLayoutParams()).knownOpen) { + return child; + } + } + return null; + } + + void moveDrawerToOffset(View drawerView, float slideOffset) { + final float oldOffset = getDrawerViewOffset(drawerView); + final int width = drawerView.getWidth(); + final int oldPos = (int) (width * oldOffset); + final int newPos = (int) (width * slideOffset); + final int dx = newPos - oldPos; + + drawerView.offsetLeftAndRight( + checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); + setDrawerViewOffset(drawerView, slideOffset); + } + + /** + * @param gravity the gravity of the child to return. If specified as a + * relative value, it will be resolved according to the current + * layout direction. + * @return the drawer with the specified gravity + */ + View findDrawerWithGravity(int gravity) { + final int absHorizGravity = GravityCompat.getAbsoluteGravity( + gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childAbsGravity = getDrawerViewAbsoluteGravity(child); + if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { + return child; + } + } + return null; + } + + /** + * Simple gravity to string - only supports LEFT and RIGHT for debugging output. + * + * @param gravity Absolute gravity value + * @return LEFT or RIGHT as appropriate, or a hex string + */ + static String gravityToString(@EdgeGravity int gravity) { + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + return "LEFT"; + } + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { + return "RIGHT"; + } + return Integer.toHexString(gravity); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + if (heightMode == MeasureSpec.AT_MOST) { + heightMode = MeasureSpec.EXACTLY; + } + else if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.EXACTLY; + heightSize = 300; + } + } else { + throw new IllegalArgumentException( + "DrawerLayout must be measured with MeasureSpec.EXACTLY."); + } + } + + setMeasuredDimension(widthSize, heightSize); + + final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); + final int layoutDirection = ViewCompat.getLayoutDirection(this); + + // Gravity value for each drawer we've seen. Only one of each permitted. + int foundDrawers = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (applyInsets) { + final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); + if (ViewCompat.getFitsSystemWindows(child)) { + IMPL.dispatchChildInsets(child, mLastInsets, cgrav); + } else { + IMPL.applyMarginInsets(lp, mLastInsets, cgrav); + } + } + + if (isContentView(child)) { + // Content views get measured at exactly the layout's size. + final int contentWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); + final int contentHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (isDrawerView(child)) { + final int childGravity = + getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((foundDrawers & childGravity) != 0) { + throw new IllegalStateException("Child drawer has absolute gravity " + + gravityToString(childGravity) + " but this " + TAG + " already has a " + + "drawer view along that edge"); + } + final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, + mMinDrawerMargin + lp.leftMargin + lp.rightMargin, + lp.width); + final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, + lp.topMargin + lp.bottomMargin, + lp.height); + child.measure(drawerWidthSpec, drawerHeightSpec); + } else { + throw new IllegalStateException("Child " + child + " at index " + i + + " does not have a valid layout_gravity - must be Gravity.LEFT, " + + "Gravity.RIGHT or Gravity.NO_GRAVITY"); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + final int width = r - l; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + child.layout(lp.leftMargin, lp.topMargin, + lp.leftMargin + child.getMeasuredWidth(), + lp.topMargin + child.getMeasuredHeight()); + } else { // Drawer, if it wasn't onMeasure would have thrown an exception. + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childLeft; + + final float newOffset; + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + childLeft = -childWidth + (int) (childWidth * lp.onScreen); + newOffset = (float) (childWidth + childLeft) / childWidth; + } else { // Right; onMeasure checked for us. + childLeft = width - (int) (childWidth * lp.onScreen); + newOffset = (float) (width - childLeft) / childWidth; + } + + final boolean changeOffset = newOffset != lp.onScreen; + + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (vgrav) { + default: + case Gravity.TOP: { + child.layout(childLeft, lp.topMargin, childLeft + childWidth, + lp.topMargin + childHeight); + break; + } + + case Gravity.BOTTOM: { + final int height = b - t; + child.layout(childLeft, + height - lp.bottomMargin - child.getMeasuredHeight(), + childLeft + childWidth, + height - lp.bottomMargin); + break; + } + + case Gravity.CENTER_VERTICAL: { + final int height = b - t; + int childTop = (height - childHeight) / 2; + + // Offset for margins. If things don't fit right because of + // bad measurement before, oh well. + if (childTop < lp.topMargin) { + childTop = lp.topMargin; + } else if (childTop + childHeight > height - lp.bottomMargin) { + childTop = height - lp.bottomMargin - childHeight; + } + child.layout(childLeft, childTop, childLeft + childWidth, + childTop + childHeight); + break; + } + } + + if (changeOffset) { + setDrawerViewOffset(child, newOffset); + } + + final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; + if (child.getVisibility() != newVisibility) { + child.setVisibility(newVisibility); + } + } + } + mInLayout = false; + mFirstLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + public void computeScroll() { + final int childCount = getChildCount(); + float scrimOpacity = 0; + for (int i = 0; i < childCount; i++) { + final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; + scrimOpacity = Math.max(scrimOpacity, onscreen); + } + mScrimOpacity = scrimOpacity; + + // "|" used on purpose; both need to run. + if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param bg Background drawable to draw behind the status bar + */ + public void setStatusBarBackground(Drawable bg) { + mStatusBarBackground = bg; + invalidate(); + } + + /** + * Gets the drawable used to draw in the insets area for the status bar. + * + * @return The status bar background drawable, or null if none set + */ + public Drawable getStatusBarBackgroundDrawable() { + return mStatusBarBackground; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param resId Resource id of a background drawable to draw behind the status bar + */ + public void setStatusBarBackground(int resId) { + mStatusBarBackground = resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null; + invalidate(); + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param color Color to use as a background drawable to draw behind the status bar + * in 0xAARRGGBB format. + */ + public void setStatusBarBackgroundColor(int color) { + mStatusBarBackground = new ColorDrawable(color); + invalidate(); + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + if (mDrawStatusBarBackground && mStatusBarBackground != null) { + final int inset = IMPL.getTopInset(mLastInsets); + if (inset > 0) { + mStatusBarBackground.setBounds(0, 0, getWidth(), inset); + mStatusBarBackground.draw(c); + } + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final int height = getHeight(); + final boolean drawingContent = isContentView(child); + int clipLeft = 0, clipRight = getWidth(); + + final int restoreCount = canvas.save(); + if (drawingContent) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == child || v.getVisibility() != VISIBLE || + !hasOpaqueBackground(v) || !isDrawerView(v) || + v.getHeight() < height) { + continue; + } + + if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { + final int vright = v.getRight(); + if (vright > clipLeft) clipLeft = vright; + } else { + final int vleft = v.getLeft(); + if (vleft < clipRight) clipRight = vleft; + } + } + canvas.clipRect(clipLeft, 0, clipRight, getHeight()); + } + final boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreCount); + + if (mScrimOpacity > 0 && drawingContent) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * mScrimOpacity); + final int color = imag << 24 | (mScrimColor & 0xffffff); + mScrimPaint.setColor(color); + + canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); + } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + final int shadowWidth = mShadowLeft.getIntrinsicWidth(); + final int childRight = child.getRight(); + final int drawerPeekDistance = mLeftDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); + mShadowLeft.setBounds(childRight, child.getTop(), + childRight + shadowWidth, child.getBottom()); + mShadowLeft.setAlpha((int) (0xff * alpha)); + mShadowLeft.draw(canvas); + } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { + final int shadowWidth = mShadowRight.getIntrinsicWidth(); + final int childLeft = child.getLeft(); + final int showing = getWidth() - childLeft; + final int drawerPeekDistance = mRightDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); + mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), + childLeft, child.getBottom()); + mShadowRight.setAlpha((int) (0xff * alpha)); + mShadowRight.draw(canvas); + } + return result; + } + + boolean isContentView(View child) { + return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; + } + + boolean isDrawerView(View child) { + final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(child)); + return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + // "|" used deliberately here; both methods should be invoked. + final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | + mRightDragger.shouldInterceptTouchEvent(ev); + + boolean interceptForTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + if (mScrimOpacity > 0) { + final View child = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (child != null && isContentView(child)) { + interceptForTap = true; + } + } + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + // If we cross the touch slop, don't perform the delayed peek for an edge touch. + if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + } + } + + return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mLeftDragger.processTouchEvent(ev); + mRightDragger.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + boolean peekingOnly = true; + final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (touchedView != null && isContentView(touchedView)) { + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mLeftDragger.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop) { + // Taps close a dimmed open drawer but only if it isn't locked open. + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; + } + } + } + closeDrawers(peekingOnly); + mDisallowInterceptRequested = false; + break; + } + + case MotionEvent.ACTION_CANCEL: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + } + + return wantTouchEvents; + } + + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (CHILDREN_DISALLOW_INTERCEPT || + (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && + !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { + // If we have an edge touch we want to skip this and track it for later instead. + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + mDisallowInterceptRequested = disallowIntercept; + if (disallowIntercept) { + closeDrawers(true); + } + } + + /** + * Close all currently open drawer views by animating them out of view. + */ + public void closeDrawers() { + closeDrawers(false); + } + + void closeDrawers(boolean peekingOnly) { + boolean needsInvalidate = false; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { + continue; + } + + final int childWidth = child.getWidth(); + + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, + -childWidth, child.getTop()); + } else { + needsInvalidate |= mRightDragger.smoothSlideViewTo(child, + getWidth(), child.getTop()); + } + + lp.isPeeking = false; + } + + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + + if (needsInvalidate) { + invalidate(); + } + } + + /** + * Open the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to open + */ + public void openDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 1.f; + lp.knownOpen = true; + + updateChildrenImportantForAccessibility(drawerView, true); + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), + drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Open the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void openDrawer(@EdgeGravity int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + openDrawer(drawerView); + } + + /** + * Close the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to close + */ + public void closeDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 0.f; + lp.knownOpen = false; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), + drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Close the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void closeDrawer(@EdgeGravity int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + closeDrawer(drawerView); + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. To check for partial visibility use + * {@link #isDrawerVisible(android.view.View)}. + * + * @param drawer Drawer view to check + * @return true if the given drawer view is in an open state + * @see #isDrawerVisible(android.view.View) + */ + public boolean isDrawerOpen(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).knownOpen; + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. If there is no drawer with the given gravity this method + * will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer view is in an open state + */ + public boolean isDrawerOpen(@EdgeGravity int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerOpen(drawerView); + } + return false; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * + * @param drawer Drawer view to check + * @return true if the given drawer is visible on-screen + * @see #isDrawerOpen(android.view.View) + */ + public boolean isDrawerVisible(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere in between. + * If there is no drawer with the given gravity this method will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer is visible on-screen + */ + public boolean isDrawerVisible(@EdgeGravity int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerVisible(drawerView); + } + return false; + } + + private boolean hasPeekingDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + if (lp.isPeeking) { + return true; + } + } + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : p instanceof ViewGroup.MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + private boolean hasVisibleDrawer() { + return findVisibleDrawer() != null; + } + + private View findVisibleDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (isDrawerView(child) && isDrawerVisible(child)) { + return child; + } + } + return null; + } + + void cancelChildViewTouch() { + // Cancel child touches + if (!mChildrenCanceledTouch) { + final long now = SystemClock.uptimeMillis(); + final MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).dispatchTouchEvent(cancelEvent); + } + cancelEvent.recycle(); + mChildrenCanceledTouch = true; + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { + KeyEventCompat.startTracking(event); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { + closeDrawers(); + } + return visibleDrawer != null; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { + final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + } + + setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); + setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + final SavedState ss = new SavedState(superState); + + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity; + } + + ss.lockModeLeft = mLockModeLeft; + ss.lockModeRight = mLockModeRight; + + return ss; + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + + final View openDrawer = findOpenDrawer(); + if (openDrawer != null || isDrawerView(child)) { + // A drawer is already open or the new view is a drawer, so the + // new view should start out hidden. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } else { + // Otherwise this is a content view and no drawer is open, so the + // new view should start out visible. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + // We only need a delegate here if the framework doesn't understand + // NO_HIDE_DESCENDANTS importance. + if (!CAN_HIDE_DESCENDANTS) { + ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate); + } + } + + private static boolean includeChildForAccessibility(View child) { + // If the child is not important for accessibility we make + // sure this hides the entire subtree rooted at it as the + // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not + // supported on older platforms but we want to hide the entire + // content and not opened drawers if a drawer is opened. + return ViewCompat.getImportantForAccessibility(child) + != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + && ViewCompat.getImportantForAccessibility(child) + != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO; + } + + /** + * State persisted across instances + */ + protected static class SavedState extends BaseSavedState { + int openDrawerGravity = Gravity.NO_GRAVITY; + int lockModeLeft = LOCK_MODE_UNLOCKED; + int lockModeRight = LOCK_MODE_UNLOCKED; + + public SavedState(Parcel in) { + super(in); + openDrawerGravity = in.readInt(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(openDrawerGravity); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel source) { + return new SavedState(source); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private final int mAbsGravity; + private ViewDragHelper mDragger; + + private final Runnable mPeekRunnable = new Runnable() { + @Override public void run() { + peekDrawer(); + } + }; + + public ViewDragCallback(int gravity) { + mAbsGravity = gravity; + } + + public void setDragger(ViewDragHelper dragger) { + mDragger = dragger; + } + + public void removeCallbacks() { + DrawerLayout.this.removeCallbacks(mPeekRunnable); + } + + @Override + public boolean tryCaptureView(View child, int pointerId) { + // Only capture views where the gravity matches what we're looking for. + // This lets us use two ViewDragHelpers, one for each side drawer. + return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) + && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; + } + + @Override + public void onViewDragStateChanged(int state) { + updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + float offset; + final int childWidth = changedView.getWidth(); + + // This reverses the positioning shown in onLayout. + if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { + offset = (float) (childWidth + left) / childWidth; + } else { + final int width = getWidth(); + offset = (float) (width - left) / childWidth; + } + setDrawerViewOffset(changedView, offset); + changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); + invalidate(); + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); + lp.isPeeking = false; + + closeOtherDrawer(); + } + + private void closeOtherDrawer() { + final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; + final View toClose = findDrawerWithGravity(otherGrav); + if (toClose != null) { + closeDrawer(toClose); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + // Offset is how open the drawer is, therefore left/right values + // are reversed from one another. + final float offset = getDrawerViewOffset(releasedChild); + final int childWidth = releasedChild.getWidth(); + + int left; + if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { + left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; + } else { + final int width = getWidth(); + left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; + } + + mDragger.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public void onEdgeTouched(int edgeFlags, int pointerId) { + postDelayed(mPeekRunnable, PEEK_DELAY); + } + + private void peekDrawer() { + final View toCapture; + final int childLeft; + final int peekDistance = mDragger.getEdgeSize(); + final boolean leftEdge = mAbsGravity == Gravity.LEFT; + if (leftEdge) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + childLeft = getWidth() - peekDistance; + } + // Only peek if it would mean making the drawer more visible and the drawer isn't locked + if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || + (!leftEdge && toCapture.getLeft() > childLeft)) && + getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); + mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); + lp.isPeeking = true; + invalidate(); + + closeOtherDrawer(); + + cancelChildViewTouch(); + } + } + + @Override + public boolean onEdgeLock(int edgeFlags) { + if (ALLOW_EDGE_LOCK) { + final View drawer = findDrawerWithGravity(mAbsGravity); + if (drawer != null && !isDrawerOpen(drawer)) { + closeDrawer(drawer); + } + return true; + } + return false; + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + final View toCapture; + if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + } + + if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + mDragger.captureChildView(toCapture, pointerId); + } + } + + @Override + public int getViewHorizontalDragRange(View child) { + return isDrawerView(child) ? child.getWidth() : 0; + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + return Math.max(-child.getWidth(), Math.min(left, 0)); + } else { + final int width = getWidth(); + return Math.max(width - child.getWidth(), Math.min(left, width)); + } + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return child.getTop(); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + float onScreen; + boolean isPeeking; + boolean knownOpen; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } + + class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + if (CAN_HIDE_DESCENDANTS) { + super.onInitializeAccessibilityNodeInfo(host, info); + } else { + // Obtain a node for the host, then manually generate the list + // of children to only include non-obscured views. + final AccessibilityNodeInfoCompat superNode = + AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + + info.setSource(host); + final ViewParent parent = ViewCompat.getParentForAccessibility(host); + if (parent instanceof View) { + info.setParent((View) parent); + } + copyNodeInfoNoChildren(info, superNode); + superNode.recycle(); + + addChildrenForAccessibility(info, (ViewGroup) host); + } + + info.setClassName(DrawerLayout.class.getName()); + + // This view reports itself as focusable so that it can intercept + // the back button, but we should prevent this view from reporting + // itself as focusable to accessibility services. + info.setFocusable(false); + info.setFocused(false); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + + event.setClassName(DrawerLayout.class.getName()); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + // Special case to handle window state change events. As far as + // accessibility services are concerned, state changes from + // DrawerLayout invalidate the entire contents of the screen (like + // an Activity or Dialog) and they should announce the title of the + // new content. + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + final List eventText = event.getText(); + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null) { + final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer); + final CharSequence title = getDrawerTitle(edgeGravity); + if (title != null) { + eventText.add(title); + } + } + + return true; + } + + return super.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { + final int childCount = v.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = v.getChildAt(i); + if (includeChildForAccessibility(child)) { + info.addChild(child); + } + } + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + } + } + + final class ChildAccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public void onInitializeAccessibilityNodeInfo(View child, + AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(child, info); + + if (!includeChildForAccessibility(child)) { + // If we are ignoring the sub-tree rooted at the child, + // break the connection to the rest of the node tree. + // For details refer to includeChildForAccessibility. + info.setParent(null); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java new file mode 100644 index 00000000000..07cc3faeaea --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java @@ -0,0 +1,94 @@ +/* + * 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. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; + +/** + * Provides functionality for DrawerLayout unique to API 21 + */ +class DrawerLayoutCompatApi21 { + + private static final int[] THEME_ATTRS = { + android.R.attr.colorPrimaryDark + }; + + public static void configureApplyInsets(View drawerLayout) { + if (drawerLayout instanceof DrawerLayoutImpl) { + drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener()); + drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + } + + public static void dispatchChildInsets(View child, Object insets, int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + child.dispatchApplyWindowInsets(wi); + } + + public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, + int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + lp.leftMargin = wi.getSystemWindowInsetLeft(); + lp.topMargin = wi.getSystemWindowInsetTop(); + lp.rightMargin = wi.getSystemWindowInsetRight(); + lp.bottomMargin = wi.getSystemWindowInsetBottom(); + } + + public static int getTopInset(Object insets) { + return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0; + } + + public static Drawable getDefaultStatusBarBackground(Context context) { + final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS); + try { + return a.getDrawable(0); + } finally { + a.recycle(); + } + } + + static class InsetsListener implements View.OnApplyWindowInsetsListener { + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v; + drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0); + return insets.consumeSystemWindowInsets(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java new file mode 100644 index 00000000000..02545c19bf2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + + +package android.support.v4.widget; + +/** + * Interface used to communicate from the v21-specific code for configuring a DrawerLayout + * to the DrawerLayout itself. + */ +interface DrawerLayoutImpl { + void setChildInsets(Object insets, boolean drawStatusBar); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java new file mode 100644 index 00000000000..55b1319f66c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.os.Build; + +/** + * Helper for accessing {@link android.widget.EdgeEffect} introduced after + * API level 4 in a backwards compatible fashion. + * + * This class is used to access {@link android.widget.EdgeEffect} on platform versions + * that support it. When running on older platforms it will result in no-ops. It should + * be used by views that wish to use the standard Android visual effects at the edges + * of scrolling containers. + */ +public class EdgeEffectCompat { + private Object mEdgeEffect; + + private static final EdgeEffectImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 21) { + IMPL = new EdgeEffectLollipopImpl(); // Lollipop + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new EdgeEffectIcsImpl(); + } else { + IMPL = new BaseEdgeEffectImpl(); + } + } + + interface EdgeEffectImpl { + public Object newEdgeEffect(Context context); + public void setSize(Object edgeEffect, int width, int height); + public boolean isFinished(Object edgeEffect); + public void finish(Object edgeEffect); + public boolean onPull(Object edgeEffect, float deltaDistance); + public boolean onRelease(Object edgeEffect); + public boolean onAbsorb(Object edgeEffect, int velocity); + public boolean draw(Object edgeEffect, Canvas canvas); + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement); + } + + /** + * Null implementation to use pre-ICS + */ + static class BaseEdgeEffectImpl implements EdgeEffectImpl { + public Object newEdgeEffect(Context context) { + return null; + } + + public void setSize(Object edgeEffect, int width, int height) { + } + + public boolean isFinished(Object edgeEffect) { + return true; + } + + public void finish(Object edgeEffect) { + } + + public boolean onPull(Object edgeEffect, float deltaDistance) { + return false; + } + + public boolean onRelease(Object edgeEffect) { + return false; + } + + public boolean onAbsorb(Object edgeEffect, int velocity) { + return false; + } + + public boolean draw(Object edgeEffect, Canvas canvas) { + return false; + } + + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return false; + } + } + + static class EdgeEffectIcsImpl implements EdgeEffectImpl { + public Object newEdgeEffect(Context context) { + return EdgeEffectCompatIcs.newEdgeEffect(context); + } + + public void setSize(Object edgeEffect, int width, int height) { + EdgeEffectCompatIcs.setSize(edgeEffect, width, height); + } + + public boolean isFinished(Object edgeEffect) { + return EdgeEffectCompatIcs.isFinished(edgeEffect); + } + + public void finish(Object edgeEffect) { + EdgeEffectCompatIcs.finish(edgeEffect); + } + + public boolean onPull(Object edgeEffect, float deltaDistance) { + return EdgeEffectCompatIcs.onPull(edgeEffect, deltaDistance); + } + + public boolean onRelease(Object edgeEffect) { + return EdgeEffectCompatIcs.onRelease(edgeEffect); + } + + public boolean onAbsorb(Object edgeEffect, int velocity) { + return EdgeEffectCompatIcs.onAbsorb(edgeEffect, velocity); + } + + public boolean draw(Object edgeEffect, Canvas canvas) { + return EdgeEffectCompatIcs.draw(edgeEffect, canvas); + } + + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return EdgeEffectCompatIcs.onPull(edgeEffect, deltaDistance); + } + } + + static class EdgeEffectLollipopImpl extends EdgeEffectIcsImpl { + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return EdgeEffectCompatLollipop.onPull(edgeEffect, deltaDistance, displacement); + } + } + + /** + * Construct a new EdgeEffect themed using the given context. + * + *

    Note: On platform versions that do not support EdgeEffect, all operations + * on the newly constructed object will be mocked/no-ops.

    + * + * @param context Context to use for theming the effect + */ + public EdgeEffectCompat(Context context) { + mEdgeEffect = IMPL.newEdgeEffect(context); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + IMPL.setSize(mEdgeEffect, width, height); + } + + /** + * Reports if this EdgeEffectCompat's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return IMPL.isFinished(mEdgeEffect); + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + IMPL.finish(mEdgeEffect); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} if this method + * returns true and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @return true if the host view should call invalidate, false if it should not. + * @deprecated use {@link #onPull(float, float)} + */ + public boolean onPull(float deltaDistance) { + return IMPL.onPull(mEdgeEffect, deltaDistance); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} if this method + * returns true and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + * @return true if the host view should call invalidate, false if it should not. + */ + public boolean onPull(float deltaDistance, float displacement) { + return IMPL.onPull(mEdgeEffect, deltaDistance, displacement); + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} if this method + * returns true and thereby draw the results accordingly. + * + * @return true if the host view should invalidate, false if it should not. + */ + public boolean onRelease() { + return IMPL.onRelease(mEdgeEffect); + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + *

    When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method getCurrVelocity will provide a reasonable approximation + * to use here.

    + * + * @param velocity Velocity at impact in pixels per second. + * @return true if the host view should invalidate, false if it should not. + */ + public boolean onAbsorb(int velocity) { + return IMPL.onAbsorb(mEdgeEffect, velocity); + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(Canvas canvas) { + return IMPL.draw(mEdgeEffect, canvas); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java new file mode 100644 index 00000000000..c02eeb4804c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.widget.EdgeEffect; + +/** + * Stub implementation that contains a real EdgeEffect on ICS. + * + * This class is an implementation detail for EdgeEffectCompat + * and should not be used directly. + */ +class EdgeEffectCompatIcs { + public static Object newEdgeEffect(Context context) { + return new EdgeEffect(context); + } + + public static void setSize(Object edgeEffect, int width, int height) { + ((EdgeEffect) edgeEffect).setSize(width, height); + } + + public static boolean isFinished(Object edgeEffect) { + return ((EdgeEffect) edgeEffect).isFinished(); + } + + public static void finish(Object edgeEffect) { + ((EdgeEffect) edgeEffect).finish(); + } + + public static boolean onPull(Object edgeEffect, float deltaDistance) { + ((EdgeEffect) edgeEffect).onPull(deltaDistance); + return true; + } + + public static boolean onRelease(Object edgeEffect) { + EdgeEffect eff = (EdgeEffect) edgeEffect; + eff.onRelease(); + return eff.isFinished(); + } + + public static boolean onAbsorb(Object edgeEffect, int velocity) { + ((EdgeEffect) edgeEffect).onAbsorb(velocity); + return true; + } + + public static boolean draw(Object edgeEffect, Canvas canvas) { + return ((EdgeEffect) edgeEffect).draw(canvas); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java new file mode 100644 index 00000000000..6ba379cf999 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java @@ -0,0 +1,27 @@ +/* + * 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.v4.widget; + +import android.widget.EdgeEffect; + +class EdgeEffectCompatLollipop { + public static boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + ((EdgeEffect) edgeEffect).onPull(deltaDistance, displacement); + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java new file mode 100644 index 00000000000..7adbc6f7ab4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java @@ -0,0 +1,725 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewParentCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityManagerCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import java.util.LinkedList; +import java.util.List; + +/** + * ExploreByTouchHelper is a utility class for implementing accessibility + * support in custom {@link View}s that represent a collection of View-like + * logical items. It extends {@link AccessibilityNodeProviderCompat} and + * simplifies many aspects of providing information to accessibility services + * and managing accessibility focus. This class does not currently support + * hierarchies of logical items. + *

    + * This should be applied to the parent view using + * {@link ViewCompat#setAccessibilityDelegate}: + * + *

    + * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
    + * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
    + * 
    + */ +public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + /** Default class name used for virtual views. */ + private static final String DEFAULT_CLASS_NAME = View.class.getName(); + + // Temporary, reusable data structures. + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + /** System accessibility manager, used to check state and send events. */ + private final AccessibilityManager mManager; + + /** View whose internal structure is exposed through this helper. */ + private final View mView; + + /** Node provider that handles creating nodes and performing actions. */ + private ExploreByTouchNodeProvider mNodeProvider; + + /** Virtual view id for the currently focused logical item. */ + private int mFocusedVirtualViewId = INVALID_ID; + + /** Virtual view id for the currently hovered logical item. */ + private int mHoveredVirtualViewId = INVALID_ID; + + /** + * Factory method to create a new {@link ExploreByTouchHelper}. + * + * @param forView View whose logical children are exposed by this helper. + */ + public ExploreByTouchHelper(View forView) { + if (forView == null) { + throw new IllegalArgumentException("View may not be null"); + } + + mView = forView; + final Context context = forView.getContext(); + mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + /** + * Returns the {@link AccessibilityNodeProviderCompat} for this helper. + * + * @param host View whose logical children are exposed by this helper. + * @return The accessibility node provider for this helper. + */ + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { + if (mNodeProvider == null) { + mNodeProvider = new ExploreByTouchNodeProvider(); + } + return mNodeProvider; + } + + /** + * Dispatches hover {@link MotionEvent}s to the virtual view hierarchy when + * the Explore by Touch feature is enabled. + *

    + * This method should be called by overriding + * {@link View#dispatchHoverEvent}: + * + *

    @Override
    +     * public boolean dispatchHoverEvent(MotionEvent event) {
    +     *   if (mHelper.dispatchHoverEvent(this, event) {
    +     *     return true;
    +     *   }
    +     *   return super.dispatchHoverEvent(event);
    +     * }
    +     * 
    + * + * @param event The hover event to dispatch to the virtual view hierarchy. + * @return Whether the hover event was handled. + */ + public boolean dispatchHoverEvent(MotionEvent event) { + if (!mManager.isEnabled() + || !AccessibilityManagerCompat.isTouchExplorationEnabled(mManager)) { + return false; + } + + switch (event.getAction()) { + case MotionEventCompat.ACTION_HOVER_MOVE: + case MotionEventCompat.ACTION_HOVER_ENTER: + final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); + updateHoveredVirtualView(virtualViewId); + return (virtualViewId != INVALID_ID); + case MotionEventCompat.ACTION_HOVER_EXIT: + if (mFocusedVirtualViewId != INVALID_ID) { + updateHoveredVirtualView(INVALID_ID); + return true; + } + return false; + default: + return false; + } + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + *

    + * You should call this method after performing a user action that normally + * fires an accessibility event, such as clicking on an item. + * + *

    public void performItemClick(T item) {
    +     *   ...
    +     *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    +     * }
    +     * 
    + * + * @param virtualViewId The virtual view id for which to send an event. + * @param eventType The type of event to send. + * @return true if the event was sent successfully. + */ + public boolean sendEventForVirtualView(int virtualViewId, int eventType) { + if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { + return false; + } + + final ViewParent parent = mView.getParent(); + if (parent == null) { + return false; + } + + final AccessibilityEvent event = createEvent(virtualViewId, eventType); + return ViewParentCompat.requestSendAccessibilityEvent(parent, mView, event); + } + + /** + * Notifies the accessibility framework that the properties of the parent + * view have changed. + *

    + * You must call this method after adding or removing items from the + * parent view. + */ + public void invalidateRoot() { + invalidateVirtualView(View.NO_ID); + } + + /** + * Notifies the accessibility framework that the properties of a particular + * item have changed. + *

    + * You must call this method after changing any of the properties set + * in {@link #onPopulateNodeForVirtualView}. + * + * @param virtualViewId The virtual view id to invalidate. + */ + public void invalidateVirtualView(int virtualViewId) { + sendEventForVirtualView( + virtualViewId, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the virtual view id for the currently focused item, + * + * @return A virtual view id, or {@link #INVALID_ID} if no item is + * currently focused. + */ + public int getFocusedVirtualView() { + return mFocusedVirtualViewId; + } + + /** + * Sets the currently hovered item, sending hover accessibility events as + * necessary to maintain the correct state. + * + * @param virtualViewId The virtual view id for the item currently being + * hovered, or {@link #INVALID_ID} if no item is hovered within + * the parent view. + */ + private void updateHoveredVirtualView(int virtualViewId) { + if (mHoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = mHoveredVirtualViewId; + mHoveredVirtualViewId = virtualViewId; + + // Stay consistent with framework behavior by sending ENTER/EXIT pairs + // in reverse order. This is accurate as of API 18. + sendEventForVirtualView(virtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualView( + previousVirtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the specified + * virtual view id, which includes the host view ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEvent(int virtualViewId, int eventType) { + switch (virtualViewId) { + case View.NO_ID: + return createEventForHost(eventType); + default: + return createEventForChild(virtualViewId, eventType); + } + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the host node. + * + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForHost(int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + ViewCompat.onInitializeAccessibilityEvent(mView, event); + return event; + } + + /** + * Constructs and returns an {@link AccessibilityEvent} populated with + * information about the specified item. + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setEnabled(true); + event.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the event. + onPopulateEventForVirtualView(virtualViewId, event); + + // Make sure the developer is following the rules. + if (event.getText().isEmpty() && (event.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateEventForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + event.setPackageName(mView.getContext().getPackageName()); + + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + record.setSource(mView, virtualViewId); + + return event; + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * specified virtual view id, which includes the host view + * ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct a node. + * @return An {@link AccessibilityNodeInfoCompat} populated with information + * about the specified item. + */ + private AccessibilityNodeInfoCompat createNode(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createNodeForHost(); + default: + return createNodeForChild(virtualViewId); + } + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * host view populated with its virtual descendants. + * + * @return An {@link AccessibilityNodeInfoCompat} for the parent node. + */ + private AccessibilityNodeInfoCompat createNodeForHost() { + final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(mView); + ViewCompat.onInitializeAccessibilityNodeInfo(mView, node); + + // Add the virtual descendants. + final LinkedList virtualViewIds = new LinkedList(); + getVisibleVirtualViews(virtualViewIds); + + for (Integer childVirtualViewId : virtualViewIds) { + node.addChild(mView, childVirtualViewId); + } + + return node; + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * specified item. Automatically manages accessibility focus actions. + *

    + * Allows the implementing class to specify most node properties, but + * overrides the following: + *

      + *
    • {@link AccessibilityNodeInfoCompat#setPackageName} + *
    • {@link AccessibilityNodeInfoCompat#setClassName} + *
    • {@link AccessibilityNodeInfoCompat#setParent(View)} + *
    • {@link AccessibilityNodeInfoCompat#setSource(View, int)} + *
    • {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} + *
    + *

    + * Uses the bounds of the parent view and the parent-relative bounding + * rectangle specified by + * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically + * update the following properties: + *

      + *
    • {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • {@link AccessibilityNodeInfoCompat#setBoundsInParent} + *
    + * + * @param virtualViewId The virtual view id for item for which to construct + * a node. + * @return An {@link AccessibilityNodeInfoCompat} for the specified item. + */ + private AccessibilityNodeInfoCompat createNodeForChild(int virtualViewId) { + final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); + + // Ensure the client has good defaults. + node.setEnabled(true); + node.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the node. + onPopulateNodeForVirtualView(virtualViewId, node); + + // Make sure the developer is following the rules. + if ((node.getText() == null) && (node.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateNodeForVirtualViewId()"); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("Callbacks must set parent bounds in " + + "populateNodeForVirtualViewId()"); + } + + final int actions = node.getActions(); + if ((actions & AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + if ((actions & AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mView.getContext().getPackageName()); + node.setSource(mView, virtualViewId); + node.setParent(mView); + + // Manage internal accessibility focus state. + if (mFocusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + private boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: + return performActionForHost(action, arguments); + default: + return performActionForChild(virtualViewId, action, arguments); + } + } + + private boolean performActionForHost(int action, Bundle arguments) { + return ViewCompat.performAccessibilityAction(mView, action, arguments); + } + + private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: + case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return manageFocusForChild(virtualViewId, action, arguments); + default: + return onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } + + private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: + return requestAccessibilityFocus(virtualViewId); + case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return clearAccessibilityFocus(virtualViewId); + default: + return false; + } + } + + /** + * Computes whether the specified {@link Rect} intersects with the visible + * portion of its parent {@link View}. Modifies {@code localRect} to contain + * only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor means that this view is not visible. + ViewParent viewParent = mView.getParent(); + while (viewParent instanceof View) { + final View view = (View) viewParent; + if ((ViewCompat.getAlpha(view) <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + viewParent = view.getParent(); + } + + // A null parent implies the view is not visible. + if (viewParent == null) { + return false; + } + + // If no portion of the parent is visible, this view is not visible. + if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + /** + * Returns whether this virtual view is accessibility focused. + * + * @return True if the view is accessibility focused. + */ + private boolean isAccessibilityFocused(int virtualViewId) { + return (mFocusedVirtualViewId == virtualViewId); + } + + /** + * Attempts to give accessibility focus to a virtual view. + *

    + * A virtual view will not actually take focus if + * {@link AccessibilityManager#isEnabled()} returns false, + * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, + * or the view already has accessibility focus. + * + * @param virtualViewId The id of the virtual view on which to place + * accessibility focus. + * @return Whether this virtual view actually took accessibility focus. + */ + private boolean requestAccessibilityFocus(int virtualViewId) { + if (!mManager.isEnabled() + || !AccessibilityManagerCompat.isTouchExplorationEnabled(mManager)) { + return false; + } + // TODO: Check virtual view visibility. + if (!isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + } + return false; + } + + /** + * Attempts to clear accessibility focus from a virtual view. + * + * @param virtualViewId The id of the virtual view from which to clear + * accessibility focus. + * @return Whether this virtual view actually cleared accessibility focus. + */ + private boolean clearAccessibilityFocus(int virtualViewId) { + if (isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = INVALID_ID; + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + } + return false; + } + + /** + * Provides a mapping between view-relative coordinates and logical + * items. + * + * @param x The view-relative x coordinate + * @param y The view-relative y coordinate + * @return virtual view identifier for the logical item under + * coordinates (x,y) or {@link View#NO_ID} if there is no item at + * the given coordinates + */ + protected abstract int getVirtualViewAt(float x, float y); + + /** + * Populates a list with the view's visible items. The ordering of items + * within {@code virtualViewIds} specifies order of accessibility focus + * traversal. + * + * @param virtualViewIds The list to populate with visible items + */ + protected abstract void getVisibleVirtualViews(List virtualViewIds); + + /** + * Populates an {@link AccessibilityEvent} with information about the + * specified item. + *

    + * Implementations must populate the following required fields: + *

      + *
    • event text, see {@link AccessibilityEvent#getText} or + * {@link AccessibilityEvent#setContentDescription} + *
    + *

    + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

      + *
    • item class name, set to android.view.View, see + * {@link AccessibilityEvent#setClassName} + *
    + *

    + * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

      + *
    • package name, set to the package of the host view's + * {@link Context}, see {@link AccessibilityEvent#setPackageName} + *
    • event source, set to the host view and virtual view identifier, + * see {@link AccessibilityRecordCompat#setSource(View, int)} + *
    + * + * @param virtualViewId The virtual view id for the item for which to + * populate the event + * @param event The event to populate + */ + protected abstract void onPopulateEventForVirtualView( + int virtualViewId, AccessibilityEvent event); + + /** + * Populates an {@link AccessibilityNodeInfoCompat} with information + * about the specified item. + *

    + * Implementations must populate the following required fields: + *

      + *
    • event text, see {@link AccessibilityNodeInfoCompat#setText} or + * {@link AccessibilityNodeInfoCompat#setContentDescription} + *
    • bounds in parent coordinates, see + * {@link AccessibilityNodeInfoCompat#setBoundsInParent} + *
    + *

    + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

      + *
    • enabled state, set to true, see + * {@link AccessibilityNodeInfoCompat#setEnabled} + *
    • item class name, identical to the class name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setClassName} + *
    + *

    + * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

      + *
    • package name, identical to the package name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setPackageName} + *
    • node source, identical to the event source set in + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setSource(View, int)} + *
    • parent view, set to the host view, see + * {@link AccessibilityNodeInfoCompat#setParent(View)} + *
    • visibility, computed based on parent-relative bounds, see + * {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • accessibility focus, computed based on internal helper state, see + * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused} + *
    • bounds in screen coordinates, computed based on host view bounds, + * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen} + *
    + *

    + * Additionally, the helper class automatically handles accessibility + * focus management by adding the appropriate + * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} or + * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * action. Implementations must never manually add these actions. + *

    + * The helper class also automatically modifies parent- and + * screen-relative bounds to reflect the portion of the item visible + * within its parent. + * + * @param virtualViewId The virtual view identifier of the item for + * which to populate the node + * @param node The node to populate + */ + protected abstract void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfoCompat node); + + /** + * Performs the specified accessibility action on the item associated + * with the virtual view identifier. See + * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)} for + * more information. + *

    + * Implementations must handle any actions added manually in + * {@link #onPopulateNodeForVirtualView}. + *

    + * The helper class automatically handles focus management resulting + * from {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} + * and + * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * actions. + * + * @param virtualViewId The virtual view identifier of the item on which + * to perform the action + * @param action The accessibility action to perform + * @param arguments (Optional) A bundle with additional arguments, or + * null + * @return true if the action was performed + */ + protected abstract boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments); + + /** + * Exposes a virtual view hierarchy to the accessibility framework. Only + * used in API 16+. + */ + private class ExploreByTouchNodeProvider extends AccessibilityNodeProviderCompat { + @Override + public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { + return ExploreByTouchHelper.this.createNode(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java new file mode 100644 index 00000000000..44222bcba83 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java @@ -0,0 +1,99 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; + +/** + * Helper for accessing features in ListPopupWindow introduced after API level 4 + * in a backwards compatible fashion. + */ +public class ListPopupWindowCompat { + /** + * Interface for the full API. + */ + interface ListPopupWindowImpl { + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BaseListPopupWindowImpl implements ListPopupWindowImpl { + @Override + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return null; + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatListPopupWindowImpl extends BaseListPopupWindowImpl { + @Override + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return ListPopupWindowCompatKitKat.createDragToOpenListener(listPopupWindow, src); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final ListPopupWindowImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatListPopupWindowImpl(); + } else { + IMPL = new BaseListPopupWindowImpl(); + } + } + + private ListPopupWindowCompat() { + // This class is not publicly instantiable. + } + + /** + * On API {@link android.os.Build.VERSION_CODES#KITKAT} and higher, returns + * an {@link OnTouchListener} that can be added to the source view to + * implement drag-to-open behavior. Generally, the source view should be the + * same view that was passed to ListPopupWindow.setAnchorView(View). + *

    + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

    + * Example usage: + * + *

    +     * ListPopupWindow myPopup = new ListPopupWindow(context);
    +     * myPopup.setAnchor(myAnchor);
    +     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
    +     * myAnchor.setOnTouchListener(dragListener);
    +     * 
    + * + * @param listPopupWindow the ListPopupWindow against which to invoke the + * method + * @param src the view on which the resulting listener will be set + * @return a touch listener that controls drag-to-open behavior, or null on + * unsupported APIs + */ + public static OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return IMPL.createDragToOpenListener(listPopupWindow, src); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java new file mode 100644 index 00000000000..7b8d8dddf81 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ListPopupWindow; + +/** + * Implementation of ListPopupWindow compatibility that can call KitKat APIs. + */ +class ListPopupWindowCompatKitKat { + public static OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return ((ListPopupWindow) listPopupWindow).createDragToOpenListener(src); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java new file mode 100644 index 00000000000..b23ca23b2cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java @@ -0,0 +1,94 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.widget.ListView; + +/** + * An implementation of {@link AutoScrollHelper} that knows how to scroll + * through a {@link ListView}. + */ +public class ListViewAutoScrollHelper extends AutoScrollHelper { + private final ListView mTarget; + + public ListViewAutoScrollHelper(ListView target) { + super(target); + + mTarget = target; + } + + @Override + public void scrollTargetBy(int deltaX, int deltaY) { + final ListView target = mTarget; + final int firstPosition = target.getFirstVisiblePosition(); + if (firstPosition == ListView.INVALID_POSITION) { + return; + } + + final View firstView = target.getChildAt(0); + if (firstView == null) { + return; + } + + final int newTop = firstView.getTop() - deltaY; + target.setSelectionFromTop(firstPosition, newTop); + } + + @Override + public boolean canTargetScrollHorizontally(int direction) { + // List do not scroll horizontally. + return false; + } + + @Override + public boolean canTargetScrollVertically(int direction) { + final ListView target = mTarget; + final int itemCount = target.getCount(); + if (itemCount == 0) { + return false; + } + + final int childCount = target.getChildCount(); + final int firstPosition = target.getFirstVisiblePosition(); + final int lastPosition = firstPosition + childCount; + + if (direction > 0) { + // Are we already showing the entire last item? + if (lastPosition >= itemCount) { + final View lastView = target.getChildAt(childCount - 1); + if (lastView.getBottom() <= target.getHeight()) { + return false; + } + } + } else if (direction < 0) { + // Are we already showing the entire first item? + if (firstPosition <= 0) { + final View firstView = target.getChildAt(0); + if (firstView.getTop() >= 0) { + return false; + } + } + } else { + // The behavior for direction 0 is undefined and we can return + // whatever we want. + return false; + } + + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java b/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java new file mode 100644 index 00000000000..db46186e32d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java @@ -0,0 +1,792 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Animatable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.DisplayMetrics; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Fancy progress indicator for Material theme. + * + * @hide + */ +class MaterialProgressDrawable extends Drawable implements Animatable { + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); + + private static final float FULL_ROTATION = 1080.0f; + @Retention(RetentionPolicy.CLASS) + @IntDef({LARGE, DEFAULT}) + public @interface ProgressDrawableSize {} + // Maps to ProgressBar.Large style + static final int LARGE = 0; + // Maps to ProgressBar default style + static final int DEFAULT = 1; + + // Maps to ProgressBar default style + private static final int CIRCLE_DIAMETER = 40; + private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width + private static final float STROKE_WIDTH = 2.5f; + + // Maps to ProgressBar.Large style + private static final int CIRCLE_DIAMETER_LARGE = 56; + private static final float CENTER_RADIUS_LARGE = 12.5f; + private static final float STROKE_WIDTH_LARGE = 3f; + + private final int[] COLORS = new int[] { + Color.BLACK + }; + + /** + * The value in the linear interpolator for animating the drawable at which + * the color transition should start + */ + private static final float COLOR_START_DELAY_OFFSET = 0.75f; + private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; + private static final float START_TRIM_DURATION_OFFSET = 0.5f; + + /** The duration of a single progress spin in milliseconds. */ + private static final int ANIMATION_DURATION = 1332; + + /** The number of points in the progress "star". */ + private static final float NUM_POINTS = 5f; + /** The list of animators operating on this drawable. */ + private final ArrayList mAnimators = new ArrayList(); + + /** The indicator ring, used to manage animation state. */ + private final Ring mRing; + + /** Canvas rotation in degrees. */ + private float mRotation; + + /** Layout info for the arrowhead in dp */ + private static final int ARROW_WIDTH = 10; + private static final int ARROW_HEIGHT = 5; + private static final float ARROW_OFFSET_ANGLE = 5; + + /** Layout info for the arrowhead for the large spinner in dp */ + private static final int ARROW_WIDTH_LARGE = 12; + private static final int ARROW_HEIGHT_LARGE = 6; + private static final float MAX_PROGRESS_ARC = .8f; + + private Resources mResources; + private View mParent; + private Animation mAnimation; + private float mRotationCount; + private double mWidth; + private double mHeight; + boolean mFinishing; + + public MaterialProgressDrawable(Context context, View parent) { + mParent = parent; + mResources = context.getResources(); + + mRing = new Ring(mCallback); + mRing.setColors(COLORS); + + updateSizes(DEFAULT); + setupAnimators(); + } + + private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, + double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { + final Ring ring = mRing; + final DisplayMetrics metrics = mResources.getDisplayMetrics(); + final float screenDensity = metrics.density; + + mWidth = progressCircleWidth * screenDensity; + mHeight = progressCircleHeight * screenDensity; + ring.setStrokeWidth((float) strokeWidth * screenDensity); + ring.setCenterRadius(centerRadius * screenDensity); + ring.setColorIndex(0); + ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); + ring.setInsets((int) mWidth, (int) mHeight); + } + + /** + * Set the overall size for the progress spinner. This updates the radius + * and stroke width of the ring. + * + * @param size One of {@link MaterialProgressDrawable.LARGE} or + * {@link MaterialProgressDrawable.DEFAULT} + */ + public void updateSizes(@ProgressDrawableSize int size) { + if (size == LARGE) { + setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, + STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); + } else { + setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, + ARROW_WIDTH, ARROW_HEIGHT); + } + } + + /** + * @param show Set to true to display the arrowhead on the progress spinner. + */ + public void showArrow(boolean show) { + mRing.setShowArrow(show); + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + mRing.setArrowScale(scale); + } + + /** + * Set the start and end trim for the progress spinner arc. + * + * @param startAngle start angle + * @param endAngle end angle + */ + public void setStartEndTrim(float startAngle, float endAngle) { + mRing.setStartTrim(startAngle); + mRing.setEndTrim(endAngle); + } + + /** + * Set the amount of rotation to apply to the progress spinner. + * + * @param rotation Rotation is from [0..1] + */ + public void setProgressRotation(float rotation) { + mRing.setRotation(rotation); + } + + /** + * Update the background color of the circle image view. + */ + public void setBackgroundColor(int color) { + mRing.setBackgroundColor(color); + } + + /** + * Set the colors used in the progress animation from color resources. + * The first color will also be the color of the bar that grows in response + * to a user swipe gesture. + * + * @param colors + */ + public void setColorSchemeColors(int... colors) { + mRing.setColors(colors); + mRing.setColorIndex(0); + } + + @Override + public int getIntrinsicHeight() { + return (int) mHeight; + } + + @Override + public int getIntrinsicWidth() { + return (int) mWidth; + } + + @Override + public void draw(Canvas c) { + final Rect bounds = getBounds(); + final int saveCount = c.save(); + c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); + mRing.draw(c, bounds); + c.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mRing.setAlpha(alpha); + } + + public int getAlpha() { + return mRing.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mRing.setColorFilter(colorFilter); + } + + @SuppressWarnings("unused") + void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + private float getRotation() { + return mRotation; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isRunning() { + final ArrayList animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final Animation animator = animators.get(i); + if (animator.hasStarted() && !animator.hasEnded()) { + return true; + } + } + return false; + } + + @Override + public void start() { + mAnimation.reset(); + mRing.storeOriginals(); + // Already showing some part of the ring + if (mRing.getEndTrim() != mRing.getStartTrim()) { + mFinishing = true; + mAnimation.setDuration(ANIMATION_DURATION/2); + mParent.startAnimation(mAnimation); + } else { + mRing.setColorIndex(0); + mRing.resetOriginals(); + mAnimation.setDuration(ANIMATION_DURATION); + mParent.startAnimation(mAnimation); + } + } + + @Override + public void stop() { + mParent.clearAnimation(); + setRotation(0); + mRing.setShowArrow(false); + mRing.setColorIndex(0); + mRing.resetOriginals(); + } + + private float getMinProgressArc(Ring ring) { + return (float) Math.toRadians( + ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); + } + + // Adapted from ArgbEvaluator.java + private int evaluateColorChange(float fraction, int startValue, int endValue) { + int startInt = (Integer) startValue; + int startA = (startInt >> 24) & 0xff; + int startR = (startInt >> 16) & 0xff; + int startG = (startInt >> 8) & 0xff; + int startB = startInt & 0xff; + + int endInt = (Integer) endValue; + int endA = (endInt >> 24) & 0xff; + int endR = (endInt >> 16) & 0xff; + int endG = (endInt >> 8) & 0xff; + int endB = endInt & 0xff; + + return (int)((startA + (int)(fraction * (endA - startA))) << 24) | + (int)((startR + (int)(fraction * (endR - startR))) << 16) | + (int)((startG + (int)(fraction * (endG - startG))) << 8) | + (int)((startB + (int)(fraction * (endB - startB)))); + } + + /** + * Update the ring color if this is within the last 25% of the animation. + * The new ring color will be a translation from the starting ring color to + * the next color. + */ + private void updateRingColor(float interpolatedTime, Ring ring) { + if (interpolatedTime > COLOR_START_DELAY_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) + / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), + ring.getNextColor())); + } + } + + private void applyFinishTranslation(float interpolatedTime, Ring ring) { + // shrink back down and complete a full rotation before + // starting other circles + // Rotation goes between [0..1]. + updateRingColor(interpolatedTime, ring); + float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + + 1f); + final float minProgressArc = getMinProgressArc(ring); + final float startTrim = ring.getStartingStartTrim() + + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) + * interpolatedTime; + ring.setStartTrim(startTrim); + ring.setEndTrim(ring.getStartingEndTrim()); + final float rotation = ring.getStartingRotation() + + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); + ring.setRotation(rotation); + } + + private void setupAnimators() { + final Ring ring = mRing; + final Animation animation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + if (mFinishing) { + applyFinishTranslation(interpolatedTime, ring); + } else { + // The minProgressArc is calculated from 0 to create an + // angle that matches the stroke width. + final float minProgressArc = getMinProgressArc(ring); + final float startingEndTrim = ring.getStartingEndTrim(); + final float startingTrim = ring.getStartingStartTrim(); + final float startingRotation = ring.getStartingRotation(); + + updateRingColor(interpolatedTime, ring); + + // Moving the start trim only occurs in the first 50% of a + // single ring animation + if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + final float scaledTime = (interpolatedTime) + / (1.0f - START_TRIM_DURATION_OFFSET); + final float startTrim = startingTrim + + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR + .getInterpolation(scaledTime)); + ring.setStartTrim(startTrim); + } + + // Moving the end trim starts after 50% of a single ring + // animation completes + if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + final float minArc = MAX_PROGRESS_ARC - minProgressArc; + float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) + / (1.0f - START_TRIM_DURATION_OFFSET); + final float endTrim = startingEndTrim + + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); + ring.setEndTrim(endTrim); + } + + final float rotation = startingRotation + (0.25f * interpolatedTime); + ring.setRotation(rotation); + + float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) + + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); + setRotation(groupRotation); + } + } + }; + animation.setRepeatCount(Animation.INFINITE); + animation.setRepeatMode(Animation.RESTART); + animation.setInterpolator(LINEAR_INTERPOLATOR); + animation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + mRotationCount = 0; + } + + @Override + public void onAnimationEnd(Animation animation) { + // do nothing + } + + @Override + public void onAnimationRepeat(Animation animation) { + ring.storeOriginals(); + ring.goToNextColor(); + ring.setStartTrim(ring.getEndTrim()); + if (mFinishing) { + // finished closing the last ring from the swipe gesture; go + // into progress mode + mFinishing = false; + animation.setDuration(ANIMATION_DURATION); + ring.setShowArrow(false); + } else { + mRotationCount = (mRotationCount + 1) % (NUM_POINTS); + } + } + }); + mAnimation = animation; + } + + private final Callback mCallback = new Callback() { + @Override + public void invalidateDrawable(Drawable d) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable d, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable d, Runnable what) { + unscheduleSelf(what); + } + }; + + private static class Ring { + private final RectF mTempBounds = new RectF(); + private final Paint mPaint = new Paint(); + private final Paint mArrowPaint = new Paint(); + + private final Callback mCallback; + + private float mStartTrim = 0.0f; + private float mEndTrim = 0.0f; + private float mRotation = 0.0f; + private float mStrokeWidth = 5.0f; + private float mStrokeInset = 2.5f; + + private int[] mColors; + // mColorIndex represents the offset into the available mColors that the + // progress circle should currently display. As the progress circle is + // animating, the mColorIndex moves by one to the next available color. + private int mColorIndex; + private float mStartingStartTrim; + private float mStartingEndTrim; + private float mStartingRotation; + private boolean mShowArrow; + private Path mArrow; + private float mArrowScale; + private double mRingCenterRadius; + private int mArrowWidth; + private int mArrowHeight; + private int mAlpha; + private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int mBackgroundColor; + private int mCurrentColor; + + public Ring(Callback callback) { + mCallback = callback; + + mPaint.setStrokeCap(Paint.Cap.SQUARE); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + + mArrowPaint.setStyle(Paint.Style.FILL); + mArrowPaint.setAntiAlias(true); + } + + public void setBackgroundColor(int color) { + mBackgroundColor = color; + } + + /** + * Set the dimensions of the arrowhead. + * + * @param width Width of the hypotenuse of the arrow head + * @param height Height of the arrow point + */ + public void setArrowDimensions(float width, float height) { + mArrowWidth = (int) width; + mArrowHeight = (int) height; + } + + /** + * Draw the progress spinner + */ + public void draw(Canvas c, Rect bounds) { + final RectF arcBounds = mTempBounds; + arcBounds.set(bounds); + arcBounds.inset(mStrokeInset, mStrokeInset); + + final float startAngle = (mStartTrim + mRotation) * 360; + final float endAngle = (mEndTrim + mRotation) * 360; + float sweepAngle = endAngle - startAngle; + + mPaint.setColor(mCurrentColor); + c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); + + drawTriangle(c, startAngle, sweepAngle, bounds); + + if (mAlpha < 255) { + mCirclePaint.setColor(mBackgroundColor); + mCirclePaint.setAlpha(255 - mAlpha); + c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, + mCirclePaint); + } + } + + private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { + if (mShowArrow) { + if (mArrow == null) { + mArrow = new android.graphics.Path(); + mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); + } else { + mArrow.reset(); + } + + // Adjust the position of the triangle so that it is inset as + // much as the arc, but also centered on the arc. + float inset = (int) mStrokeInset / 2 * mArrowScale; + float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); + float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); + + // Update the path each time. This works around an issue in SKIA + // where concatenating a rotation matrix to a scale matrix + // ignored a starting negative rotation. This appears to have + // been fixed as of API 21. + mArrow.moveTo(0, 0); + mArrow.lineTo(mArrowWidth * mArrowScale, 0); + mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight + * mArrowScale)); + mArrow.offset(x - inset, y); + mArrow.close(); + // draw a triangle + mArrowPaint.setColor(mCurrentColor); + c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), + bounds.exactCenterY()); + c.drawPath(mArrow, mArrowPaint); + } + } + + /** + * Set the colors the progress spinner alternates between. + * + * @param colors Array of integers describing the colors. Must be non-null. + */ + public void setColors(@NonNull int[] colors) { + mColors = colors; + // if colors are reset, make sure to reset the color index as well + setColorIndex(0); + } + + /** + * Set the absolute color of the progress spinner. This is should only + * be used when animating between current and next color when the + * spinner is rotating. + * + * @param color int describing the color. + */ + public void setColor(int color) { + mCurrentColor = color; + } + + /** + * @param index Index into the color array of the color to display in + * the progress spinner. + */ + public void setColorIndex(int index) { + mColorIndex = index; + mCurrentColor = mColors[mColorIndex]; + } + + /** + * @return int describing the next color the progress spinner should use when drawing. + */ + public int getNextColor() { + return mColors[getNextColorIndex()]; + } + + private int getNextColorIndex() { + return (mColorIndex + 1) % (mColors.length); + } + + /** + * Proceed to the next available ring color. This will automatically + * wrap back to the beginning of colors. + */ + public void goToNextColor() { + setColorIndex(getNextColorIndex()); + } + + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + invalidateSelf(); + } + + /** + * @param alpha Set the alpha of the progress spinner and associated arrowhead. + */ + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + /** + * @return Current alpha of the progress spinner and arrowhead. + */ + public int getAlpha() { + return mAlpha; + } + + /** + * @param strokeWidth Set the stroke width of the progress spinner in pixels. + */ + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + mPaint.setStrokeWidth(strokeWidth); + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStrokeWidth() { + return mStrokeWidth; + } + + @SuppressWarnings("unused") + public void setStartTrim(float startTrim) { + mStartTrim = startTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStartTrim() { + return mStartTrim; + } + + public float getStartingStartTrim() { + return mStartingStartTrim; + } + + public float getStartingEndTrim() { + return mStartingEndTrim; + } + + public int getStartingColor() { + return mColors[mColorIndex]; + } + + @SuppressWarnings("unused") + public void setEndTrim(float endTrim) { + mEndTrim = endTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getEndTrim() { + return mEndTrim; + } + + @SuppressWarnings("unused") + public void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getRotation() { + return mRotation; + } + + public void setInsets(int width, int height) { + final float minEdge = (float) Math.min(width, height); + float insets; + if (mRingCenterRadius <= 0 || minEdge < 0) { + insets = (float) Math.ceil(mStrokeWidth / 2.0f); + } else { + insets = (float) (minEdge / 2.0f - mRingCenterRadius); + } + mStrokeInset = insets; + } + + @SuppressWarnings("unused") + public float getInsets() { + return mStrokeInset; + } + + /** + * @param centerRadius Inner radius in px of the circle the progress + * spinner arc traces. + */ + public void setCenterRadius(double centerRadius) { + mRingCenterRadius = centerRadius; + } + + public double getCenterRadius() { + return mRingCenterRadius; + } + + /** + * @param show Set to true to show the arrow head on the progress spinner. + */ + public void setShowArrow(boolean show) { + if (mShowArrow != show) { + mShowArrow = show; + invalidateSelf(); + } + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + if (scale != mArrowScale) { + mArrowScale = scale; + invalidateSelf(); + } + } + + /** + * @return The amount the progress spinner is currently rotated, between [0..1]. + */ + public float getStartingRotation() { + return mStartingRotation; + } + + /** + * If the start / end trim are offset to begin with, store them so that + * animation starts from that offset. + */ + public void storeOriginals() { + mStartingStartTrim = mStartTrim; + mStartingEndTrim = mEndTrim; + mStartingRotation = mRotation; + } + + /** + * Reset the progress spinner to default rotation, start and end angles. + */ + public void resetOriginals() { + mStartingStartTrim = 0; + mStartingEndTrim = 0; + mStartingRotation = 0; + setStartTrim(0); + setEndTrim(0); + setRotation(0); + } + + private void invalidateSelf() { + mCallback.invalidateDrawable(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java b/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java new file mode 100644 index 00000000000..eb2481491c6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java @@ -0,0 +1,1862 @@ +/* + * 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.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.InputDeviceCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; +import android.support.v4.view.NestedScrollingParent; +import android.support.v4.view.NestedScrollingParentHelper; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +import java.util.List; + +/** + * NestedScrollView is just like {@link android.widget.ScrollView}, but it supports acting + * as both a nested scrolling parent and child on both new and old versions of Android. + * Nested scrolling is enabled by default. + */ +public class NestedScrollView extends FrameLayout implements NestedScrollingParent, + NestedScrollingChild { + static final int ANIMATED_SCROLL_GAP = 250; + + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = "NestedScrollView"; + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private ScrollerCompat mScroller; + private EdgeEffectCompat mEdgeGlowTop; + private EdgeEffectCompat mEdgeGlowBottom; + + /** + * Position of the last motion event. + */ + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedYOffset; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[] { + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + + public NestedScrollView(Context context) { + this(context, null); + } + + public NestedScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + + setFillViewport(a.getBoolean(0, false)); + + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + } + + @Override + public void onStopNestedScroll(View target) { + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollY = getScrollY(); + scrollBy(0, dyUnconsumed); + final int myConsumed = getScrollY() - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + // Do nothing + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + // Do nothing + return false; + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = new ScrollerCompat(getContext(), null); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + * + * @attr ref android.R.styleable#ScrollView_fillViewport + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Indicates this ScrollView whether it should stretch its content height to fill + * the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + * + * @attr ref android.R.styleable#ScrollView_fillViewport + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int height = getMeasuredHeight(); + if (child.getMeasuredHeight() < height) { + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() + || x >= child.getRight()); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + /* + * Don't try to intercept touch if we can't scroll anyway. + */ + if (getScrollY() == 0 && !ViewCompat.canScrollVertically(this, 1)) { + return false; + } + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int y = (int) MotionEventCompat.getY(ev, pointerIndex); + final int yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop + && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { + mIsBeingDragged = true; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (!inChild((int) ev.getX(), (int) y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + stopNestedScroll(); + break; + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = MotionEventCompat.getActionMasked(ev); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + } + vtev.offsetLocation(0, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, + mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y - mScrollOffset[1]; + + final int oldY = getScrollY(); + final int range = getScrollRange(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + range > 0); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, + 0, true) && !hasNestedScrollingParent()) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight(), + MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight(), + 1.f - MotionEventCompat.getX(ev, activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, + mActivePointerId); + + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocity); + } + + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionY = (int) MotionEventCompat.getY(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) MotionEventCompat.getY(ev, + MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> + MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEventCompat.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float vscroll = MotionEventCompat.getAxisValue(event, + MotionEventCompat.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); + final int range = getScrollRange(); + int oldScrollY = getScrollY(); + int newScrollY = oldScrollY - delta; + if (newScrollY < 0) { + newScrollY = 0; + } else if (newScrollY > range) { + newScrollY = range; + } + if (newScrollY != oldScrollY) { + super.scrollTo(getScrollX(), newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = ViewCompat.getOverScrollMode(this); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + private int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

    + * Finds the next focusable component that fits in the specified bounds. + *

    + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && + (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate + .getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

    Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward, {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScrollY(delta); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScrollY(int delta) { + if (delta != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(0, delta); + } else { + scrollBy(0, delta); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int bottom = getChildAt(0).getHeight(); + final int maxY = Math.max(0, bottom - height); + final int scrollY = getScrollY(); + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + + mScroller.startScroll(getScrollX(), scrollY, 0, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

    The scroll range of a scroll view is the overall height of all of its + * children.

    + */ + @Override + protected int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + @Override + protected int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + + getPaddingRight(), lp.width); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + final boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, 0, false); + + if (canOverscroll) { + ensureGlows(); + if (y <= 0 && oldY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= range && oldY < range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + + int fadingEdge = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + * + * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(getScrollX(), mSavedState.scrollPosition); + mSavedState = null; + } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRange = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + if (getScrollY() > scrollRange) { + scrollTo(getScrollX(), scrollRange); + } else if (getScrollY() < 0) { + scrollTo(getScrollX(), 0); + } + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + + mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height/2); + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityY) { + final int scrollY = getScrollY(); + final boolean canFling = (scrollY > 0 || velocityY > 0) && + (scrollY < getScrollRange() || velocityY < 0); + if (!dispatchNestedPreFling(0, velocityY)) { + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { + fling(velocityY); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + } + + /** + * {@inheritDoc} + * + *

    This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffectCompat(context); + mEdgeGlowBottom = new EdgeEffectCompat(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRange(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my+n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child-my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + + SavedState(Parcelable superState) { + super(superState); + } + + public SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final NestedScrollView nsvHost = (NestedScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRange()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final NestedScrollView nsvHost = (NestedScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRange(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final NestedScrollView nsvHost = (NestedScrollView) host; + event.setClassName(ScrollView.class.getName()); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + final boolean scrollable = nsvHost.getScrollRange() > 0; + record.setScrollable(scrollable); + record.setScrollX(nsvHost.getScrollX()); + record.setScrollY(nsvHost.getScrollY()); + record.setMaxScrollX(nsvHost.getScrollX()); + record.setMaxScrollY(nsvHost.getScrollRange()); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java new file mode 100644 index 00000000000..db0e2c98495 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View.OnTouchListener; + +/** + * Helper for accessing features in PopupMenu introduced after API level 4 in a + * backwards compatible fashion. + */ +public class PopupMenuCompat { + /** + * Interface for the full API. + */ + interface PopupMenuImpl { + public OnTouchListener getDragToOpenListener(Object popupMenu); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BasePopupMenuImpl implements PopupMenuImpl { + @Override + public OnTouchListener getDragToOpenListener(Object popupMenu) { + return null; + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatPopupMenuImpl extends BasePopupMenuImpl { + @Override + public OnTouchListener getDragToOpenListener(Object popupMenu) { + return PopupMenuCompatKitKat.getDragToOpenListener(popupMenu); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final PopupMenuImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatPopupMenuImpl(); + } else { + IMPL = new BasePopupMenuImpl(); + } + } + + private PopupMenuCompat() { + // This class is not publicly instantiable. + } + + /** + * On API {@link android.os.Build.VERSION_CODES#KITKAT} and higher, returns + * an {@link OnTouchListener} that can be added to the anchor view to + * implement drag-to-open behavior. + *

    + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

    + * Example usage: + *

    +     * PopupMenu myPopup = new PopupMenu(context, myAnchor);
    +     * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
    +     * 
    + * + * @param popupMenu the PopupMenu against which to invoke the method + * @return a touch listener that controls drag-to-open behavior, or null on + * unsupported APIs + */ + public static OnTouchListener getDragToOpenListener(Object popupMenu) { + return IMPL.getDragToOpenListener(popupMenu); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java new file mode 100644 index 00000000000..45587981a82 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View.OnTouchListener; +import android.widget.PopupMenu; + +/** + * Implementation of PopupMenu compatibility that can call KitKat APIs. + */ +class PopupMenuCompatKitKat { + public static OnTouchListener getDragToOpenListener(Object popupMenu) { + return ((PopupMenu) popupMenu).getDragToOpenListener(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java new file mode 100644 index 00000000000..be96a739a51 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java @@ -0,0 +1,95 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.PopupWindow; + +/** + * Helper for accessing features in PopupWindow introduced after API level 4 + * in a backwards compatible fashion. + */ +public class PopupWindowCompat { + /** + * Interface for the full API. + */ + interface PopupWindowImpl { + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BasePopupWindowImpl implements PopupWindowImpl { + @Override + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + popup.showAsDropDown(anchor, xoff, yoff); + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatPopupWindowImpl extends BasePopupWindowImpl { + @Override + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + PopupWindowCompatKitKat.showAsDropDown(popup, anchor, xoff, yoff, gravity); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final PopupWindowImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatPopupWindowImpl(); + } else { + IMPL = new BasePopupWindowImpl(); + } + } + + private PopupWindowCompat() { + // This class is not publicly instantiable. + } + + /** + *

    Display the content view in a popup window anchored to the bottom-left + * corner of the anchor view offset by the specified x and y coordinates. + * If there is not enough room on screen to show + * the popup in its entirety, this method tries to find a parent scroll + * view to scroll. If no parent scroll view can be scrolled, the bottom-left + * corner of the popup is pinned at the top left corner of the anchor view.

    + *

    If the view later scrolls to move anchor to a different + * location, the popup will be moved correspondingly.

    + * + * @param popup the PopupWindow to show + * @param anchor the view on which to pin the popup window + * @param xoff A horizontal offset from the anchor in pixels + * @param yoff A vertical offset from the anchor in pixels + * @param gravity Alignment of the popup relative to the anchor + */ + public static void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + IMPL.showAsDropDown(popup, anchor, xoff, yoff, gravity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java new file mode 100644 index 00000000000..4333f4a37da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; + +/** + * Implementation of PopupWindow compatibility that can call KitKat APIs. + */ +class PopupWindowCompatKitKat { + public static void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + popup.showAsDropDown(anchor, xoff, yoff, gravity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java new file mode 100644 index 00000000000..c854cf55086 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +/** + * Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class ResourceCursorAdapter extends CursorAdapter { + private int mLayout; + + private int mDropDownLayout; + + private LayoutInflater mInflater; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + */ + @Deprecated + public ResourceCursorAdapter(Context context, int layout, Cursor c) { + super(context, c); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Constructor with default behavior as per + * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended + * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout Resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, int flags) { + super(context, c, flags); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Inflates view(s) from the specified XML file. + * + * @see android.widget.CursorAdapter#newView(android.content.Context, + * android.database.Cursor, ViewGroup) + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mLayout, parent, false); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mDropDownLayout, parent, false); + } + + /** + *

    Sets the layout resource of the item views.

    + * + * @param layout the layout resources used to create item views + */ + public void setViewResource(int layout) { + mLayout = layout; + } + + /** + *

    Sets the layout resource of the drop down views.

    + * + * @param dropDownLayout the layout resources used to create drop down views + */ + public void setDropDownViewResource(int dropDownLayout) { + mDropDownLayout = dropDownLayout; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java new file mode 100644 index 00000000000..afbf89704e0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java @@ -0,0 +1,484 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.os.Build; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +/** + * Provides access to new {@link android.widget.Scroller Scroller} APIs when available. + * + *

    This class provides a platform version-independent mechanism for obeying the + * current device's preferred scroll physics and fling behavior. It offers a subset of + * the APIs from Scroller or OverScroller.

    + */ +public class ScrollerCompat { + private static final String TAG = "ScrollerCompat"; + + Object mScroller; + ScrollerCompatImpl mImpl; + + interface ScrollerCompatImpl { + Object createScroller(Context context, Interpolator interpolator); + boolean isFinished(Object scroller); + int getCurrX(Object scroller); + int getCurrY(Object scroller); + float getCurrVelocity(Object scroller); + boolean computeScrollOffset(Object scroller); + void startScroll(Object scroller, int startX, int startY, int dx, int dy); + void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration); + void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY); + void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY); + void abortAnimation(Object scroller); + void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX); + void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY); + boolean isOverScrolled(Object scroller); + int getFinalX(Object scroller); + int getFinalY(Object scroller); + } + + static final int CHASE_FRAME_TIME = 16; // ms per target frame + + static class ScrollerCompatImplBase implements ScrollerCompatImpl { + @Override + public Object createScroller(Context context, Interpolator interpolator) { + return interpolator != null ? + new Scroller(context, interpolator) : new Scroller(context); + } + + @Override + public boolean isFinished(Object scroller) { + return ((Scroller) scroller).isFinished(); + } + + @Override + public int getCurrX(Object scroller) { + return ((Scroller) scroller).getCurrX(); + } + + @Override + public int getCurrY(Object scroller) { + return ((Scroller) scroller).getCurrY(); + } + + @Override + public float getCurrVelocity(Object scroller) { + return 0; + } + + @Override + public boolean computeScrollOffset(Object scroller) { + final Scroller s = (Scroller) scroller; + return s.computeScrollOffset(); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ((Scroller) scroller).startScroll(startX, startY, dx, dy); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + @Override + public void abortAnimation(Object scroller) { + ((Scroller) scroller).abortAnimation(); + } + + @Override + public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + // No-op + } + + @Override + public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + // No-op + } + + @Override + public boolean isOverScrolled(Object scroller) { + // Always false + return false; + } + + @Override + public int getFinalX(Object scroller) { + return ((Scroller) scroller).getFinalX(); + } + + @Override + public int getFinalY(Object scroller) { + return ((Scroller) scroller).getFinalY(); + } + } + + static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl { + @Override + public Object createScroller(Context context, Interpolator interpolator) { + return ScrollerCompatGingerbread.createScroller(context, interpolator); + } + + @Override + public boolean isFinished(Object scroller) { + return ScrollerCompatGingerbread.isFinished(scroller); + } + + @Override + public int getCurrX(Object scroller) { + return ScrollerCompatGingerbread.getCurrX(scroller); + } + + @Override + public int getCurrY(Object scroller) { + return ScrollerCompatGingerbread.getCurrY(scroller); + } + + @Override + public float getCurrVelocity(Object scroller) { + return 0; + } + + @Override + public boolean computeScrollOffset(Object scroller) { + return ScrollerCompatGingerbread.computeScrollOffset(scroller); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, + minX, maxX, minY, maxY); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, + minX, maxX, minY, maxY, overX, overY); + } + + @Override + public void abortAnimation(Object scroller) { + ScrollerCompatGingerbread.abortAnimation(scroller); + } + + @Override + public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX); + } + + @Override + public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY); + } + + @Override + public boolean isOverScrolled(Object scroller) { + return ScrollerCompatGingerbread.isOverScrolled(scroller); + } + + @Override + public int getFinalX(Object scroller) { + return ScrollerCompatGingerbread.getFinalX(scroller); + } + + @Override + public int getFinalY(Object scroller) { + return ScrollerCompatGingerbread.getFinalY(scroller); + } + } + + static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread { + @Override + public float getCurrVelocity(Object scroller) { + return ScrollerCompatIcs.getCurrVelocity(scroller); + } + } + + public static ScrollerCompat create(Context context) { + return create(context, null); + } + + public static ScrollerCompat create(Context context, Interpolator interpolator) { + return new ScrollerCompat(context, interpolator); + } + + ScrollerCompat(Context context, Interpolator interpolator) { + this(Build.VERSION.SDK_INT, context, interpolator); + + } + + /** + * Private constructer where API version can be provided. + * Useful for unit testing. + */ + private ScrollerCompat(int apiVersion, Context context, Interpolator interpolator) { + if (apiVersion >= 14) { // ICS + mImpl = new ScrollerCompatImplIcs(); + } else if (apiVersion>= 9) { // Gingerbread + mImpl = new ScrollerCompatImplGingerbread(); + } else { + mImpl = new ScrollerCompatImplBase(); + } + mScroller = mImpl.createScroller(context, interpolator); + } + + /** + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public boolean isFinished() { + return mImpl.isFinished(mScroller); + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public int getCurrX() { + return mImpl.getCurrX(mScroller); + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public int getCurrY() { + return mImpl.getCurrY(mScroller); + } + + /** + * @return The final X position for the scroll in progress, if known. + */ + public int getFinalX() { + return mImpl.getFinalX(mScroller); + } + + /** + * @return The final Y position for the scroll in progress, if known. + */ + public int getFinalY() { + return mImpl.getFinalY(mScroller); + } + + /** + * Returns the current velocity on platform versions that support it. + * + *

    The device must support at least API level 14 (Ice Cream Sandwich). + * On older platform versions this method will return 0. This method should + * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.

    + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + public float getCurrVelocity() { + return mImpl.getCurrVelocity(mScroller); + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + public boolean computeScrollOffset() { + return mImpl.computeScrollOffset(mScroller); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + mImpl.startScroll(mScroller, startX, startY, dx, dy); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mImpl.startScroll(mScroller, startX, startY, dx, dy, duration); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + * @param overX Overfling range. If > 0, horizontal overfling in either + * direction will be possible. + * @param overY Overfling range. If > 0, vertical overfling in either + * direction will be possible. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + mImpl.fling(mScroller, startX, startY, velocityX, velocityY, + minX, maxX, minY, maxY, overX, overY); + } + + /** + * Stops the animation. Aborting the animation causes the scroller to move to the final x and y + * position. + */ + public void abortAnimation() { + mImpl.abortAnimation(mScroller); + } + + + /** + * Notify the scroller that we've reached a horizontal boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will transition the current motion and + * animate from startX to finalX as appropriate. + * + * @param startX Starting/current X position + * @param finalX Desired final X position + * @param overX Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalX. Absolute value - must be positive. + */ + public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { + mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX); + } + + /** + * Notify the scroller that we've reached a vertical boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will animate a parabolic motion from + * startY to finalY. + * + * @param startY Starting/current Y position + * @param finalY Desired final Y position + * @param overY Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalY. Absolute value - must be positive. + */ + public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { + mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY); + } + + /** + * Returns whether the current Scroller is currently returning to a valid position. + * Valid bounds were provided by the + * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. + * + * One should check this value before calling + * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress + * to restore a valid position will then be stopped. The caller has to take into account + * the fact that the started scroll will start from an overscrolled position. + * + * @return true when the current position is overscrolled and in the process of + * interpolating back to a valid value. + */ + public boolean isOverScrolled() { + return mImpl.isOverScrolled(mScroller); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java new file mode 100644 index 00000000000..429e864b087 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java @@ -0,0 +1,90 @@ +/* + * 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. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.view.animation.Interpolator; +import android.widget.OverScroller; + +class ScrollerCompatGingerbread { + public static Object createScroller(Context context, Interpolator interpolator) { + return interpolator != null ? + new OverScroller(context, interpolator) : new OverScroller(context); + } + + public static boolean isFinished(Object scroller) { + return ((OverScroller) scroller).isFinished(); + } + + public static int getCurrX(Object scroller) { + return ((OverScroller) scroller).getCurrX(); + } + + public static int getCurrY(Object scroller) { + return ((OverScroller) scroller).getCurrY(); + } + + public static boolean computeScrollOffset(Object scroller) { + return ((OverScroller) scroller).computeScrollOffset(); + } + + public static void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ((OverScroller) scroller).startScroll(startX, startY, dx, dy); + } + + public static void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ((OverScroller) scroller).startScroll(startX, startY, dx, dy, duration); + } + + public static void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ((OverScroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + public static void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ((OverScroller) scroller).fling(startX, startY, velX, velY, + minX, maxX, minY, maxY, overX, overY); + } + + public static void abortAnimation(Object scroller) { + ((OverScroller) scroller).abortAnimation(); + } + + public static void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + ((OverScroller) scroller).notifyHorizontalEdgeReached(startX, finalX, overX); + } + + public static void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + ((OverScroller) scroller).notifyVerticalEdgeReached(startY, finalY, overY); + } + + public static boolean isOverScrolled(Object scroller) { + return ((OverScroller) scroller).isOverScrolled(); + } + + public static int getFinalX(Object scroller) { + return ((OverScroller) scroller).getFinalX(); + } + + public static int getFinalY(Object scroller) { + return ((OverScroller) scroller).getFinalY(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java new file mode 100644 index 00000000000..76b9a2b595d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.widget.OverScroller; + +/** + * ICS API access for ScrollerCompat + */ +class ScrollerCompatIcs { + public static float getCurrVelocity(Object scroller) { + return ((OverScroller) scroller).getCurrVelocity(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java new file mode 100644 index 00000000000..c51ecf27b1b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.widget.TextView; + +/** + * Helper for accessing features in {@link android.widget.SearchView} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class SearchViewCompat { + + interface SearchViewCompatImpl { + View newSearchView(Context context); + void setSearchableInfo(View searchView, ComponentName searchableComponent); + void setImeOptions(View searchView, int imeOptions); + void setInputType(View searchView, int inputType); + Object newOnQueryTextListener(OnQueryTextListenerCompat listener); + void setOnQueryTextListener(Object searchView, Object listener); + Object newOnCloseListener(OnCloseListenerCompat listener); + void setOnCloseListener(Object searchView, Object listener); + CharSequence getQuery(View searchView); + void setQuery(View searchView, CharSequence query, boolean submit); + void setQueryHint(View searchView, CharSequence hint); + void setIconified(View searchView, boolean iconify); + boolean isIconified(View searchView); + void setSubmitButtonEnabled(View searchView, boolean enabled); + boolean isSubmitButtonEnabled(View searchView); + void setQueryRefinementEnabled(View searchView, boolean enable); + boolean isQueryRefinementEnabled(View searchView); + void setMaxWidth(View searchView, int maxpixels); + } + + static class SearchViewCompatStubImpl implements SearchViewCompatImpl { + + @Override + public View newSearchView(Context context) { + return null; + } + + @Override + public void setSearchableInfo(View searchView, ComponentName searchableComponent) { + } + + @Override + public void setImeOptions(View searchView, int imeOptions) { + } + + @Override + public void setInputType(View searchView, int inputType) { + } + + @Override + public Object newOnQueryTextListener(OnQueryTextListenerCompat listener) { + return null; + } + + @Override + public void setOnQueryTextListener(Object searchView, Object listener) { + } + + @Override + public Object newOnCloseListener(OnCloseListenerCompat listener) { + return null; + } + + @Override + public void setOnCloseListener(Object searchView, Object listener) { + } + + @Override + public CharSequence getQuery(View searchView) { + return null; + } + + @Override + public void setQuery(View searchView, CharSequence query, boolean submit) { + } + + @Override + public void setQueryHint(View searchView, CharSequence hint) { + } + + @Override + public void setIconified(View searchView, boolean iconify) { + } + + @Override + public boolean isIconified(View searchView) { + return true; + } + + @Override + public void setSubmitButtonEnabled(View searchView, boolean enabled) { + } + + @Override + public boolean isSubmitButtonEnabled(View searchView) { + return false; + } + + @Override + public void setQueryRefinementEnabled(View searchView, boolean enable) { + } + + @Override + public boolean isQueryRefinementEnabled(View searchView) { + return false; + } + + @Override + public void setMaxWidth(View searchView, int maxpixels) { + } + } + + static class SearchViewCompatHoneycombImpl extends SearchViewCompatStubImpl { + + @Override + public View newSearchView(Context context) { + return SearchViewCompatHoneycomb.newSearchView(context); + } + + @Override + public void setSearchableInfo(View searchView, ComponentName searchableComponent) { + SearchViewCompatHoneycomb.setSearchableInfo(searchView, searchableComponent); + } + + @Override + public Object newOnQueryTextListener(final OnQueryTextListenerCompat listener) { + return SearchViewCompatHoneycomb.newOnQueryTextListener( + new SearchViewCompatHoneycomb.OnQueryTextListenerCompatBridge() { + @Override + public boolean onQueryTextSubmit(String query) { + return listener.onQueryTextSubmit(query); + } + @Override + public boolean onQueryTextChange(String newText) { + return listener.onQueryTextChange(newText); + } + }); + } + + @Override + public void setOnQueryTextListener(Object searchView, Object listener) { + SearchViewCompatHoneycomb.setOnQueryTextListener(searchView, listener); + } + + @Override + public Object newOnCloseListener(final OnCloseListenerCompat listener) { + return SearchViewCompatHoneycomb.newOnCloseListener( + new SearchViewCompatHoneycomb.OnCloseListenerCompatBridge() { + @Override + public boolean onClose() { + return listener.onClose(); + } + }); + } + + @Override + public void setOnCloseListener(Object searchView, Object listener) { + SearchViewCompatHoneycomb.setOnCloseListener(searchView, listener); + } + + @Override + public CharSequence getQuery(View searchView) { + return SearchViewCompatHoneycomb.getQuery(searchView); + } + + @Override + public void setQuery(View searchView, CharSequence query, boolean submit) { + SearchViewCompatHoneycomb.setQuery(searchView, query, submit); + } + + @Override + public void setQueryHint(View searchView, CharSequence hint) { + SearchViewCompatHoneycomb.setQueryHint(searchView, hint); + } + + @Override + public void setIconified(View searchView, boolean iconify) { + SearchViewCompatHoneycomb.setIconified(searchView, iconify); + } + + @Override + public boolean isIconified(View searchView) { + return SearchViewCompatHoneycomb.isIconified(searchView); + } + + @Override + public void setSubmitButtonEnabled(View searchView, boolean enabled) { + SearchViewCompatHoneycomb.setSubmitButtonEnabled(searchView, enabled); + } + + @Override + public boolean isSubmitButtonEnabled(View searchView) { + return SearchViewCompatHoneycomb.isSubmitButtonEnabled(searchView); + } + + @Override + public void setQueryRefinementEnabled(View searchView, boolean enable) { + SearchViewCompatHoneycomb.setQueryRefinementEnabled(searchView, enable); + } + + @Override + public boolean isQueryRefinementEnabled(View searchView) { + return SearchViewCompatHoneycomb.isQueryRefinementEnabled(searchView); + } + + @Override + public void setMaxWidth(View searchView, int maxpixels) { + SearchViewCompatHoneycomb.setMaxWidth(searchView, maxpixels); + } + } + + static class SearchViewCompatIcsImpl extends SearchViewCompatHoneycombImpl { + + @Override + public View newSearchView(Context context) { + return SearchViewCompatIcs.newSearchView(context); + } + + @Override + public void setImeOptions(View searchView, int imeOptions) { + SearchViewCompatIcs.setImeOptions(searchView, imeOptions); + } + + @Override + public void setInputType(View searchView, int inputType) { + SearchViewCompatIcs.setInputType(searchView, inputType); + } + } + + private static final SearchViewCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new SearchViewCompatIcsImpl(); + } else if (Build.VERSION.SDK_INT >= 11) { // Honeycomb + IMPL = new SearchViewCompatHoneycombImpl(); + } else { + IMPL = new SearchViewCompatStubImpl(); + } + } + + private SearchViewCompat(Context context) { + /* Hide constructor */ + } + + /** + * Creates a new SearchView. + * + * @param context The Context the view is running in. + * @return A SearchView instance if the class is present on the current + * platform, null otherwise. + */ + public static View newSearchView(Context context) { + return IMPL.newSearchView(context); + } + + /** + * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used + * to display labels, hints, suggestions, create intents for launching search results screens + * and controlling other affordances such as a voice button. + * + * @param searchView The SearchView to operate on. + * @param searchableComponent The application component whose + * {@link android.app.SearchableInfo} should be loaded and applied to + * the SearchView. + */ + public static void setSearchableInfo(View searchView, ComponentName searchableComponent) { + IMPL.setSearchableInfo(searchView, searchableComponent); + } + + /** + * Sets the IME options on the query text field. This is a no-op if + * called on pre-{@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} + * platforms. + * + * @see TextView#setImeOptions(int) + * @param searchView The SearchView to operate on. + * @param imeOptions the options to set on the query text field + */ + public static void setImeOptions(View searchView, int imeOptions) { + IMPL.setImeOptions(searchView, imeOptions); + } + + /** + * Sets the input type on the query text field. This is a no-op if + * called on pre-{@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} + * platforms. + * + * @see TextView#setInputType(int) + * @param searchView The SearchView to operate on. + * @param inputType the input type to set on the query text field + */ + public static void setInputType(View searchView, int inputType) { + IMPL.setInputType(searchView, inputType); + } + + /** + * Sets a listener for user actions within the SearchView. + * + * @param searchView The SearchView in which to register the listener. + * @param listener the listener object that receives callbacks when the user performs + * actions in the SearchView such as clicking on buttons or typing a query. + */ + public static void setOnQueryTextListener(View searchView, OnQueryTextListenerCompat listener) { + IMPL.setOnQueryTextListener(searchView, listener.mListener); + } + + /** + * Callbacks for changes to the query text. + */ + public static abstract class OnQueryTextListenerCompat { + final Object mListener; + + public OnQueryTextListenerCompat() { + mListener = IMPL.newOnQueryTextListener(this); + } + + /** + * Called when the user submits the query. This could be due to a key press on the + * keyboard or due to pressing a submit button. + * The listener can override the standard behavior by returning true + * to indicate that it has handled the submit request. Otherwise return false to + * let the SearchView handle the submission by launching any associated intent. + * + * @param query the query text that is to be submitted + * + * @return true if the query has been handled by the listener, false to let the + * SearchView perform the default action. + */ + public boolean onQueryTextSubmit(String query) { + return false; + } + + /** + * Called when the query text is changed by the user. + * + * @param newText the new content of the query text field. + * + * @return false if the SearchView should perform the default action of showing any + * suggestions if available, true if the action was handled by the listener. + */ + public boolean onQueryTextChange(String newText) { + return false; + } + } + + /** + * Sets a listener to inform when the user closes the SearchView. + * + * @param searchView The SearchView in which to register the listener. + * @param listener the listener to call when the user closes the SearchView. + */ + public static void setOnCloseListener(View searchView, OnCloseListenerCompat listener) { + IMPL.setOnCloseListener(searchView, listener.mListener); + } + + /** + * Callback for closing the query UI. + */ + public static abstract class OnCloseListenerCompat { + final Object mListener; + + public OnCloseListenerCompat() { + mListener = IMPL.newOnCloseListener(this); + } + + /** + * The user is attempting to close the SearchView. + * + * @return true if the listener wants to override the default behavior of clearing the + * text field and dismissing it, false otherwise. + */ + public boolean onClose() { + return false; + } + } + + /** + * Returns the query string currently in the text field. + * + * @param searchView The SearchView to operate on. + * + * @return the query string + */ + public static CharSequence getQuery(View searchView) { + return IMPL.getQuery(searchView); + } + + /** + * Sets a query string in the text field and optionally submits the query as well. + * + * @param searchView The SearchView to operate on. + * @param query the query string. This replaces any query text already present in the + * text field. + * @param submit whether to submit the query right now or only update the contents of + * text field. + */ + public static void setQuery(View searchView, CharSequence query, boolean submit) { + IMPL.setQuery(searchView, query, submit); + } + + /** + * Sets the hint text to display in the query text field. This overrides any hint specified + * in the SearchableInfo. + * + * @param searchView The SearchView to operate on. + * @param hint the hint text to display + */ + public static void setQueryHint(View searchView, CharSequence hint) { + IMPL.setQueryHint(searchView, hint); + } + + /** + * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is + * a temporary state and does not override the default iconified state set by + * setIconifiedByDefault(boolean). If the default state is iconified, then + * a false here will only be valid until the user closes the field. And if the default + * state is expanded, then a true here will only clear the text field and not close it. + * + * @param searchView The SearchView to operate on. + * @param iconify a true value will collapse the SearchView to an icon, while a false will + * expand it. + */ + public static void setIconified(View searchView, boolean iconify) { + IMPL.setIconified(searchView, iconify); + } + + /** + * Returns the current iconified state of the SearchView. + * + * @param searchView The SearchView to operate on. + * @return true if the SearchView is currently iconified, false if the search field is + * fully visible. + */ + public static boolean isIconified(View searchView) { + return IMPL.isIconified(searchView); + } + + /** + * Enables showing a submit button when the query is non-empty. In cases where the SearchView + * is being used to filter the contents of the current activity and doesn't launch a separate + * results activity, then the submit button should be disabled. + * + * @param searchView The SearchView to operate on. + * @param enabled true to show a submit button for submitting queries, false if a submit + * button is not required. + */ + public static void setSubmitButtonEnabled(View searchView, boolean enabled) { + IMPL.setSubmitButtonEnabled(searchView, enabled); + } + + /** + * Returns whether the submit button is enabled when necessary or never displayed. + * + * @param searchView The SearchView to operate on. + * @return whether the submit button is enabled automatically when necessary + */ + public static boolean isSubmitButtonEnabled(View searchView) { + return IMPL.isSubmitButtonEnabled(searchView); + } + + /** + * Specifies if a query refinement button should be displayed alongside each suggestion + * or if it should depend on the flags set in the individual items retrieved from the + * suggestions provider. Clicking on the query refinement button will replace the text + * in the query text field with the text from the suggestion. This flag only takes effect + * if a SearchableInfo has been specified with {@link #setSearchableInfo(View, ComponentName)} + * and not when using a custom adapter. + * + * @param searchView The SearchView to operate on. + * @param enable true if all items should have a query refinement button, false if only + * those items that have a query refinement flag set should have the button. + * + * @see SearchManager#SUGGEST_COLUMN_FLAGS + * @see SearchManager#FLAG_QUERY_REFINEMENT + */ + public static void setQueryRefinementEnabled(View searchView, boolean enable) { + IMPL.setQueryRefinementEnabled(searchView, enable); + } + + /** + * Returns whether query refinement is enabled for all items or only specific ones. + * @param searchView The SearchView to operate on. + * @return true if enabled for all items, false otherwise. + */ + public static boolean isQueryRefinementEnabled(View searchView) { + return IMPL.isQueryRefinementEnabled(searchView); + } + + /** + * Makes the view at most this many pixels wide + * @param searchView The SearchView to operate on. + */ + public static void setMaxWidth(View searchView, int maxpixels) { + IMPL.setMaxWidth(searchView, maxpixels); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java new file mode 100644 index 00000000000..eeadfabb332 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.Context; +import android.view.View; +import android.widget.SearchView; +import android.widget.SearchView.OnCloseListener; +import android.widget.SearchView.OnQueryTextListener; + +/** + * Implementation of SearchView compatibility that can call Honeycomb APIs. + */ +class SearchViewCompatHoneycomb { + + interface OnQueryTextListenerCompatBridge { + public boolean onQueryTextSubmit(String query); + public boolean onQueryTextChange(String newText); + } + + interface OnCloseListenerCompatBridge { + public boolean onClose(); + } + + public static View newSearchView(Context context) { + return new SearchView(context); + } + + public static void setSearchableInfo(View searchView, ComponentName searchableComponent) { + SearchView sv = ((SearchView) searchView); + SearchManager searchManager = (SearchManager) + sv.getContext().getSystemService(Context.SEARCH_SERVICE); + sv.setSearchableInfo(searchManager.getSearchableInfo(searchableComponent)); + } + + public static Object newOnQueryTextListener(final OnQueryTextListenerCompatBridge listener) { + return new OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return listener.onQueryTextSubmit(query); + } + + @Override + public boolean onQueryTextChange(String newText) { + return listener.onQueryTextChange(newText); + } + }; + } + + public static void setOnQueryTextListener(Object searchView, Object listener) { + ((SearchView) searchView).setOnQueryTextListener((OnQueryTextListener) listener); + } + + public static Object newOnCloseListener(final OnCloseListenerCompatBridge listener) { + return new OnCloseListener() { + @Override + public boolean onClose() { + return listener.onClose(); + } + }; + } + + public static void setOnCloseListener(Object searchView, Object listener) { + ((SearchView) searchView).setOnCloseListener((OnCloseListener) listener); + } + + public static CharSequence getQuery(View searchView) { + return ((SearchView) searchView).getQuery(); + } + + public static void setQuery(View searchView, CharSequence query, boolean submit) { + ((SearchView) searchView).setQuery(query, submit); + } + + public static void setQueryHint(View searchView, CharSequence hint) { + ((SearchView) searchView).setQueryHint(hint); + } + + public static void setIconified(View searchView, boolean iconify) { + ((SearchView) searchView).setIconified(iconify); + } + + public static boolean isIconified(View searchView) { + return ((SearchView) searchView).isIconified(); + } + + public static void setSubmitButtonEnabled(View searchView, boolean enabled) { + ((SearchView) searchView).setSubmitButtonEnabled(enabled); + } + + public static boolean isSubmitButtonEnabled(View searchView) { + return ((SearchView) searchView).isSubmitButtonEnabled(); + } + + public static void setQueryRefinementEnabled(View searchView, boolean enable) { + ((SearchView) searchView).setQueryRefinementEnabled(enable); + } + + public static boolean isQueryRefinementEnabled(View searchView) { + return ((SearchView) searchView).isQueryRefinementEnabled(); + } + + public static void setMaxWidth(View searchView, int maxpixels) { + ((SearchView) searchView).setMaxWidth(maxpixels); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java new file mode 100644 index 00000000000..b8d719cb8e4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.view.View; +import android.widget.SearchView; + +/** + * Implementation of SearchView compatibility that can call ICS APIs. + */ +class SearchViewCompatIcs { + + public static class MySearchView extends SearchView { + public MySearchView(Context context) { + super(context); + } + + // The normal SearchView doesn't clear its search text when + // collapsed, so we will do this for it. + @Override + public void onActionViewCollapsed() { + setQuery("", false); + super.onActionViewCollapsed(); + } + } + + public static View newSearchView(Context context) { + return new MySearchView(context); + } + + public static void setImeOptions(View searchView, int imeOptions) { + ((SearchView) searchView).setImeOptions(imeOptions); + } + + public static void setInputType(View searchView, int inputType) { + ((SearchView) searchView).setInputType(inputType); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java new file mode 100644 index 00000000000..ca71e9ecc57 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Static library support version of the framework's {@link android.widget.SimpleCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class SimpleCursorAdapter extends ResourceCursorAdapter { + /** + * A list of columns containing the data to bind to the UI. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mFrom; + /** + * A list of View ids representing the views to which the data must be bound. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mTo; + + private int mStringConversionColumn = -1; + private CursorToStringConverter mCursorToStringConverter; + private ViewBinder mViewBinder; + + String[] mOriginalFrom; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { + super(context, layout, c); + mTo = to; + mOriginalFrom = from; + findColumns(from); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. The layout file should include at least + * those named views defined in "to" + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, + int[] to, int flags) { + super(context, layout, c, flags); + mTo = to; + mOriginalFrom = from; + findColumns(from); + } + + /** + * Binds all of the field names passed into the "to" parameter of the + * constructor with their corresponding cursor columns as specified in the + * "from" parameter. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value is + * false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException if binding cannot occur + * + * @see android.widget.CursorAdapter#bindView(android.view.View, + * android.content.Context, android.database.Cursor) + * @see #getViewBinder() + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + * @see #setViewImage(ImageView, String) + * @see #setViewText(TextView, String) + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + final ViewBinder binder = mViewBinder; + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + boolean bound = false; + if (binder != null) { + bound = binder.setViewValue(v, cursor, from[i]); + } + + if (!bound) { + String text = cursor.getString(from[i]); + if (text == null) { + text = ""; + } + + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleCursorAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an TextView. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + /** + * Return the index of the column used to get a String representation + * of the Cursor. + * + * @return a valid index in the current Cursor or -1 + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #setStringConversionColumn(int) + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public int getStringConversionColumn() { + return mStringConversionColumn; + } + + /** + * Defines the index of the column in the Cursor used to get a String + * representation of that Cursor. The column is used to convert the + * Cursor to a String only when the current CursorToStringConverter + * is null. + * + * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default + * conversion mechanism + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #getStringConversionColumn() + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public void setStringConversionColumn(int stringConversionColumn) { + mStringConversionColumn = stringConversionColumn; + } + + /** + * Returns the converter used to convert the filtering Cursor + * into a String. + * + * @return null if the converter does not exist or an instance of + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public CursorToStringConverter getCursorToStringConverter() { + return mCursorToStringConverter; + } + + /** + * Sets the converter used to convert the filtering Cursor + * into a String. + * + * @param cursorToStringConverter the Cursor to String converter, or + * null to remove the converter + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { + mCursorToStringConverter = cursorToStringConverter; + } + + /** + * Returns a CharSequence representation of the specified Cursor as defined + * by the current CursorToStringConverter. If no CursorToStringConverter + * has been set, the String conversion column is used instead. If the + * conversion column is -1, the returned String is empty if the cursor + * is null or Cursor.toString(). + * + * @param cursor the Cursor to convert to a CharSequence + * + * @return a non-null CharSequence representing the cursor + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (mCursorToStringConverter != null) { + return mCursorToStringConverter.convertToString(cursor); + } else if (mStringConversionColumn > -1) { + return cursor.getString(mStringConversionColumn); + } + + return super.convertToString(cursor); + } + + /** + * Create a map from an array of strings to an array of column-id integers in mCursor. + * If mCursor is null, the array will be discarded. + * + * @param from the Strings naming the columns of interest + */ + private void findColumns(String[] from) { + if (mCursor != null) { + int i; + int count = from.length; + if (mFrom == null || mFrom.length != count) { + mFrom = new int[count]; + } + for (i = 0; i < count; i++) { + mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]); + } + } else { + mFrom = null; + } + } + + @Override + public Cursor swapCursor(Cursor c) { + Cursor res = super.swapCursor(c); + // rescan columns in case cursor layout is different + findColumns(mOriginalFrom); + return res; + } + + /** + * Change the cursor and change the column-to-view mappings at the same time. + * + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + */ + public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { + mOriginalFrom = from; + mTo = to; + super.changeCursor(c); + findColumns(mOriginalFrom); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to bind values fom the Cursor to views. + * + * You should use this class to bind values from the Cursor to views + * that are not directly supported by SimpleCursorAdapter or to + * change the way binding occurs for views supported by + * SimpleCursorAdapter. + * + * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see SimpleCursorAdapter#setViewImage(ImageView, String) + * @see SimpleCursorAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the Cursor column defined by the specified index to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleCursorAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param cursor the cursor to get the data from + * @param columnIndex the column at which the data can be found in the cursor + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Cursor cursor, int columnIndex); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to define how the Cursor should be converted to a String. + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public static interface CursorToStringConverter { + /** + * Returns a CharSequence representing the specified Cursor. + * + * @param cursor the cursor for which a CharSequence representation + * is requested + * + * @return a non-null CharSequence representing the cursor + */ + CharSequence convertToString(Cursor cursor); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java new file mode 100644 index 00000000000..391ba999556 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java @@ -0,0 +1,1644 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.DrawableRes; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; + +/** + * SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level + * of a UI. A left (or first) pane is treated as a content list or browser, subordinate to a + * primary detail view for displaying content. + * + *

    Child views may overlap if their combined width exceeds the available width + * in the SlidingPaneLayout. When this occurs the user may slide the topmost view out of the way + * by dragging it, or by navigating in the direction of the overlapped view using a keyboard. + * If the content of the dragged child view is itself horizontally scrollable, the user may + * grab it by the very edge.

    + * + *

    Thanks to this sliding behavior, SlidingPaneLayout may be suitable for creating layouts + * that can smoothly adapt across many different screen sizes, expanding out fully on larger + * screens and collapsing on smaller screens.

    + * + *

    SlidingPaneLayout is distinct from a navigation drawer as described in the design + * guide and should not be used in the same scenarios. SlidingPaneLayout should be thought + * of only as a way to allow a two-pane layout normally used on larger screens to adapt to smaller + * screens in a natural way. The interaction patterns expressed by SlidingPaneLayout imply + * a physicality and direct information hierarchy between panes that does not necessarily exist + * in a scenario where a navigation drawer should be used instead.

    + * + *

    Appropriate uses of SlidingPaneLayout include pairings of panes such as a contact list and + * subordinate interactions with those contacts, or an email thread list with the content pane + * displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include + * switching between disparate functions of your app, such as jumping from a social stream view + * to a view of your personal profile - cases such as this should use the navigation drawer + * pattern instead. ({@link DrawerLayout DrawerLayout} implements this pattern.)

    + * + *

    Like {@link android.widget.LinearLayout LinearLayout}, SlidingPaneLayout supports + * the use of the layout parameter layout_weight on child views to determine + * how to divide leftover space after measurement is complete. It is only relevant for width. + * When views do not overlap weight behaves as it does in a LinearLayout.

    + * + *

    When views do overlap, weight on a slideable pane indicates that the pane should be + * sized to fill all available space in the closed state. Weight on a pane that becomes covered + * indicates that the pane should be sized to fill all available space except a small minimum strip + * that the user may use to grab the slideable view and pull it back over into a closed state.

    + */ +public class SlidingPaneLayout extends ViewGroup { + private static final String TAG = "SlidingPaneLayout"; + + /** + * Default size of the overhang for a pane in the open state. + * At least this much of a sliding pane will remain visible. + * This indicates that there is more content available and provides + * a "physical" edge to grab to pull it closed. + */ + private static final int DEFAULT_OVERHANG_SIZE = 32; // dp; + + /** + * If no fade color is given by default it will fade to 80% gray. + */ + private static final int DEFAULT_FADE_COLOR = 0xcccccccc; + + /** + * The fade color used for the sliding panel. 0 = no fading. + */ + private int mSliderFadeColor = DEFAULT_FADE_COLOR; + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * The fade color used for the panel covered by the slider. 0 = no fading. + */ + private int mCoveredFadeColor; + + /** + * Drawable used to draw the shadow between panes by default. + */ + private Drawable mShadowDrawableLeft; + + /** + * Drawable used to draw the shadow between panes to support RTL (right to left language). + */ + private Drawable mShadowDrawableRight; + + /** + * The size of the overhang in pixels. + * This is the minimum section of the sliding panel that will + * be visible in the open state to allow for a closing drag. + */ + private final int mOverhangSize; + + /** + * True if a panel can slide with the current measurements + */ + private boolean mCanSlide; + + /** + * The child view that can slide, if any. + */ + private View mSlideableView; + + /** + * How far the panel is offset from its closed position. + * range [0, 1] where 0 = closed, 1 = open. + */ + private float mSlideOffset; + + /** + * How far the non-sliding panel is parallaxed from its usual position when open. + * range [0, 1] + */ + private float mParallaxOffset; + + /** + * How far in pixels the slideable panel may move. + */ + private int mSlideRange; + + /** + * A panel view is locked into internal scrolling or another condition that + * is preventing a drag. + */ + private boolean mIsUnableToDrag; + + /** + * Distance in pixels to parallax the fixed pane by when fully closed + */ + private int mParallaxBy; + + private float mInitialMotionX; + private float mInitialMotionY; + + private PanelSlideListener mPanelSlideListener; + + private final ViewDragHelper mDragHelper; + + /** + * Stores whether or not the pane was open the last time it was slideable. + * If open/close operations are invoked this state is modified. Used by + * instance state save/restore. + */ + private boolean mPreservedOpenState; + private boolean mFirstLayout = true; + + private final Rect mTmpRect = new Rect(); + + private final ArrayList mPostedRunnables = + new ArrayList(); + + static final SlidingPanelLayoutImpl IMPL; + + static { + final int deviceVersion = Build.VERSION.SDK_INT; + if (deviceVersion >= 17) { + IMPL = new SlidingPanelLayoutImplJBMR1(); + } else if (deviceVersion >= 16) { + IMPL = new SlidingPanelLayoutImplJB(); + } else { + IMPL = new SlidingPanelLayoutImplBase(); + } + } + + /** + * Listener for monitoring events about sliding panes. + */ + public interface PanelSlideListener { + /** + * Called when a sliding pane's position changes. + * @param panel The child view that was moved + * @param slideOffset The new offset of this sliding pane within its range, from 0-1 + */ + public void onPanelSlide(View panel, float slideOffset); + /** + * Called when a sliding pane becomes slid completely open. The pane may or may not + * be interactive at this point depending on how much of the pane is visible. + * @param panel The child view that was slid to an open position, revealing other panes + */ + public void onPanelOpened(View panel); + + /** + * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed + * to be interactive. It may now obscure other views in the layout. + * @param panel The child view that was slid to a closed position + */ + public void onPanelClosed(View panel); + } + + /** + * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset + * of the listener methods you can extend this instead of implement the full interface. + */ + public static class SimplePanelSlideListener implements PanelSlideListener { + @Override + public void onPanelSlide(View panel, float slideOffset) { + } + @Override + public void onPanelOpened(View panel) { + } + @Override + public void onPanelClosed(View panel) { + } + } + + public SlidingPaneLayout(Context context) { + this(context, null); + } + + public SlidingPaneLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final float density = context.getResources().getDisplayMetrics().density; + mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f); + + final ViewConfiguration viewConfig = ViewConfiguration.get(context); + + setWillNotDraw(false); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + + mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); + mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); + } + + /** + * Set a distance to parallax the lower pane by when the upper pane is in its + * fully closed state. The lower pane will scroll between this position and + * its fully open state. + * + * @param parallaxBy Distance to parallax by in pixels + */ + public void setParallaxDistance(int parallaxBy) { + mParallaxBy = parallaxBy; + requestLayout(); + } + + /** + * @return The distance the lower pane will parallax by when the upper pane is fully closed. + * + * @see #setParallaxDistance(int) + */ + public int getParallaxDistance() { + return mParallaxBy; + } + + /** + * Set the color used to fade the sliding pane out when it is slid most of the way offscreen. + * + * @param color An ARGB-packed color value + */ + public void setSliderFadeColor(int color) { + mSliderFadeColor = color; + } + + /** + * @return The ARGB-packed color value used to fade the sliding pane + */ + public int getSliderFadeColor() { + return mSliderFadeColor; + } + + /** + * Set the color used to fade the pane covered by the sliding pane out when the pane + * will become fully covered in the closed state. + * + * @param color An ARGB-packed color value + */ + public void setCoveredFadeColor(int color) { + mCoveredFadeColor = color; + } + + /** + * @return The ARGB-packed color value used to fade the fixed pane + */ + public int getCoveredFadeColor() { + return mCoveredFadeColor; + } + + public void setPanelSlideListener(PanelSlideListener listener) { + mPanelSlideListener = listener; + } + + void dispatchOnPanelSlide(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelSlide(panel, mSlideOffset); + } + } + + void dispatchOnPanelOpened(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelOpened(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelClosed(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelClosed(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void updateObscuredViewsVisibility(View panel) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + final int startBound = isLayoutRtl ? (getWidth() - getPaddingRight()) : + getPaddingLeft(); + final int endBound = isLayoutRtl ? getPaddingLeft() : + (getWidth() - getPaddingRight()); + final int topBound = getPaddingTop(); + final int bottomBound = getHeight() - getPaddingBottom(); + final int left; + final int right; + final int top; + final int bottom; + if (panel != null && viewIsOpaque(panel)) { + left = panel.getLeft(); + right = panel.getRight(); + top = panel.getTop(); + bottom = panel.getBottom(); + } else { + left = right = top = bottom = 0; + } + + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + + if (child == panel) { + // There are still more children above the panel but they won't be affected. + break; + } + + final int clampedChildLeft = Math.max((isLayoutRtl ? endBound : + startBound), child.getLeft()); + final int clampedChildTop = Math.max(topBound, child.getTop()); + final int clampedChildRight = Math.min((isLayoutRtl ? startBound : + endBound), child.getRight()); + final int clampedChildBottom = Math.min(bottomBound, child.getBottom()); + final int vis; + if (clampedChildLeft >= left && clampedChildTop >= top && + clampedChildRight <= right && clampedChildBottom <= bottom) { + vis = INVISIBLE; + } else { + vis = VISIBLE; + } + child.setVisibility(vis); + } + } + + void setAllChildrenVisible() { + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == INVISIBLE) { + child.setVisibility(VISIBLE); + } + } + } + + private static boolean viewIsOpaque(View v) { + if (ViewCompat.isOpaque(v)) return true; + + // View#isOpaque didn't take all valid opaque scrollbar modes into account + // before API 18 (JB-MR2). On newer devices rely solely on isOpaque above and return false + // here. On older devices, check the view's background drawable directly as a fallback. + if (Build.VERSION.SDK_INT >= 18) return false; + + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + + for (int i = 0, count = mPostedRunnables.size(); i < count; i++) { + final DisableLayerRunnable dlr = mPostedRunnables.get(i); + dlr.run(); + } + mPostedRunnables.clear(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + } else { + throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); + } + } else if (heightMode == MeasureSpec.UNSPECIFIED) { + if (isInEditMode()) { + // Don't crash the layout editor. Pick a magic number from thin air instead. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.AT_MOST; + heightSize = 300; + } + } else { + throw new IllegalStateException("Height must not be UNSPECIFIED"); + } + } + + int layoutHeight = 0; + int maxLayoutHeight = -1; + switch (heightMode) { + case MeasureSpec.EXACTLY: + layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + break; + case MeasureSpec.AT_MOST: + maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + break; + } + + float weightSum = 0; + boolean canSlide = false; + final int widthAvailable = widthSize - getPaddingLeft() - getPaddingRight(); + int widthRemaining = widthAvailable; + final int childCount = getChildCount(); + + if (childCount > 2) { + Log.e(TAG, "onMeasure: More than two child views are not supported."); + } + + // We'll find the current one below. + mSlideableView = null; + + // First pass. Measure based on child LayoutParams width/height. + // Weight will incur a second pass. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE) { + lp.dimWhenOffset = false; + continue; + } + + if (lp.weight > 0) { + weightSum += lp.weight; + + // If we have no width, weight is the only contributor to the final size. + // Measure this view on the weight pass only. + if (lp.width == 0) continue; + } + + int childWidthSpec; + final int horizontalMargin = lp.leftMargin + lp.rightMargin; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, + MeasureSpec.AT_MOST); + } else if (lp.width == LayoutParams.FILL_PARENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, + MeasureSpec.EXACTLY); + } else { + childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + } + + int childHeightSpec; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } + + child.measure(childWidthSpec, childHeightSpec); + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + if (heightMode == MeasureSpec.AT_MOST && childHeight > layoutHeight) { + layoutHeight = Math.min(childHeight, maxLayoutHeight); + } + + widthRemaining -= childWidth; + canSlide |= lp.slideable = widthRemaining < 0; + if (lp.slideable) { + mSlideableView = child; + } + } + + // Resolve weight and make sure non-sliding panels are smaller than the full screen. + if (canSlide || weightSum > 0) { + final int fixedPanelWidthLimit = widthAvailable - mOverhangSize; + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE) { + continue; + } + + final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0; + final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth(); + if (canSlide && child != mSlideableView) { + if (lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.weight > 0)) { + // Fixed panels in a sliding configuration should + // be clamped to the fixed panel limit. + final int childHeightSpec; + if (skippedFirstPass) { + // Do initial height measurement if we skipped measuring this view + // the first time around. + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + } + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec( + child.getMeasuredHeight(), MeasureSpec.EXACTLY); + } + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + fixedPanelWidthLimit, MeasureSpec.EXACTLY); + child.measure(childWidthSpec, childHeightSpec); + } + } else if (lp.weight > 0) { + int childHeightSpec; + if (lp.width == 0) { + // This was skipped the first time; figure out a real height spec. + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + } + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec( + child.getMeasuredHeight(), MeasureSpec.EXACTLY); + } + + if (canSlide) { + // Consume available space + final int horizontalMargin = lp.leftMargin + lp.rightMargin; + final int newWidth = widthAvailable - horizontalMargin; + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + newWidth, MeasureSpec.EXACTLY); + if (measuredWidth != newWidth) { + child.measure(childWidthSpec, childHeightSpec); + } + } else { + // Distribute the extra width proportionally similar to LinearLayout + final int widthToDistribute = Math.max(0, widthRemaining); + final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum); + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + measuredWidth + addedWidth, MeasureSpec.EXACTLY); + child.measure(childWidthSpec, childHeightSpec); + } + } + } + } + + final int measuredWidth = widthSize; + final int measuredHeight = layoutHeight + getPaddingTop() + getPaddingBottom(); + + setMeasuredDimension(measuredWidth, measuredHeight); + mCanSlide = canSlide; + + if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) { + // Cancel scrolling in progress, it's no longer relevant. + mDragHelper.abort(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + if (isLayoutRtl) { + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + } else { + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + } + final int width = r - l; + final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft(); + final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight(); + final int paddingTop = getPaddingTop(); + + final int childCount = getChildCount(); + int xStart = paddingStart; + int nextXStart = xStart; + + if (mFirstLayout) { + mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f; + } + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int childWidth = child.getMeasuredWidth(); + int offset = 0; + + if (lp.slideable) { + final int margin = lp.leftMargin + lp.rightMargin; + final int range = Math.min(nextXStart, + width - paddingEnd - mOverhangSize) - xStart - margin; + mSlideRange = range; + final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin; + lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 > + width - paddingEnd; + final int pos = (int) (range * mSlideOffset); + xStart += pos + lpMargin; + mSlideOffset = (float) pos / mSlideRange; + } else if (mCanSlide && mParallaxBy != 0) { + offset = (int) ((1 - mSlideOffset) * mParallaxBy); + xStart = nextXStart; + } else { + xStart = nextXStart; + } + + final int childRight; + final int childLeft; + if (isLayoutRtl) { + childRight = width - xStart + offset; + childLeft = childRight - childWidth; + } else { + childLeft = xStart - offset; + childRight = childLeft + childWidth; + } + + final int childTop = paddingTop; + final int childBottom = childTop + child.getMeasuredHeight(); + child.layout(childLeft, paddingTop, childRight, childBottom); + + nextXStart += child.getWidth(); + } + + if (mFirstLayout) { + if (mCanSlide) { + if (mParallaxBy != 0) { + parallaxOtherViews(mSlideOffset); + } + if (((LayoutParams) mSlideableView.getLayoutParams()).dimWhenOffset) { + dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor); + } + } else { + // Reset the dim level of all children; it's irrelevant when nothing moves. + for (int i = 0; i < childCount; i++) { + dimChildView(getChildAt(i), 0, mSliderFadeColor); + } + } + updateObscuredViewsVisibility(mSlideableView); + } + + mFirstLayout = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Recalculate sliding panes and their details + if (w != oldw) { + mFirstLayout = true; + } + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (!isInTouchMode() && !mCanSlide) { + mPreservedOpenState = child == mSlideableView; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + // Preserve the open state based on the last view that was touched. + if (!mCanSlide && action == MotionEvent.ACTION_DOWN && getChildCount() > 1) { + // After the first things will be slideable. + final View secondChild = getChildAt(1); + if (secondChild != null) { + mPreservedOpenState = !mDragHelper.isViewUnder(secondChild, + (int) ev.getX(), (int) ev.getY()); + } + } + + if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { + mDragHelper.cancel(); + return super.onInterceptTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mDragHelper.cancel(); + return false; + } + + boolean interceptTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIsUnableToDrag = false; + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + + if (mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y) && + isDimmed(mSlideableView)) { + interceptTap = true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + final float x = ev.getX(); + final float y = ev.getY(); + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int slop = mDragHelper.getTouchSlop(); + if (adx > slop && ady > adx) { + mDragHelper.cancel(); + mIsUnableToDrag = true; + return false; + } + } + } + + final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); + + return interceptForDrag || interceptTap; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!mCanSlide) { + return super.onTouchEvent(ev); + } + + mDragHelper.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + break; + } + + case MotionEvent.ACTION_UP: { + if (isDimmed(mSlideableView)) { + final float x = ev.getX(); + final float y = ev.getY(); + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mDragHelper.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop && + mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y)) { + // Taps close a dimmed open pane. + closePane(mSlideableView, 0); + break; + } + } + break; + } + } + + return wantTouchEvents; + } + + private boolean closePane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(0.f, initialVelocity)) { + mPreservedOpenState = false; + return true; + } + return false; + } + + private boolean openPane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) { + mPreservedOpenState = true; + return true; + } + return false; + } + + /** + * @deprecated Renamed to {@link #openPane()} - this method is going away soon! + */ + @Deprecated + public void smoothSlideOpen() { + openPane(); + } + + /** + * Open the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now open/in the process of opening + */ + public boolean openPane() { + return openPane(mSlideableView, 0); + } + + /** + * @deprecated Renamed to {@link #closePane()} - this method is going away soon! + */ + @Deprecated + public void smoothSlideClosed() { + closePane(); + } + + /** + * Close the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now closed/in the process of closing + */ + public boolean closePane() { + return closePane(mSlideableView, 0); + } + + /** + * Check if the layout is completely open. It can be open either because the slider + * itself is open revealing the left pane, or if all content fits without sliding. + * + * @return true if sliding panels are completely open + */ + public boolean isOpen() { + return !mCanSlide || mSlideOffset == 1; + } + + /** + * @return true if content in this layout can be slid open and closed + * @deprecated Renamed to {@link #isSlideable()} - this method is going away soon! + */ + @Deprecated + public boolean canSlide() { + return mCanSlide; + } + + /** + * Check if the content in this layout cannot fully fit side by side and therefore + * the content pane can be slid back and forth. + * + * @return true if content in this layout can be slid open and closed + */ + public boolean isSlideable() { + return mCanSlide; + } + + private void onPanelDragged(int newLeft) { + if (mSlideableView == null) { + // This can happen if we're aborting motion during layout because everything now fits. + mSlideOffset = 0; + return; + } + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + int childWidth = mSlideableView.getWidth(); + final int newStart = isLayoutRtl ? getWidth() - newLeft - childWidth : newLeft; + + final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft(); + final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin; + final int startBound = paddingStart + lpMargin; + + mSlideOffset = (float) (newStart - startBound) / mSlideRange; + + if (mParallaxBy != 0) { + parallaxOtherViews(mSlideOffset); + } + + if (lp.dimWhenOffset) { + dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor); + } + dispatchOnPanelSlide(mSlideableView); + } + + private void dimChildView(View v, float mag, int fadeColor) { + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + + if (mag > 0 && fadeColor != 0) { + final int baseAlpha = (fadeColor & 0xff000000) >>> 24; + int imag = (int) (baseAlpha * mag); + int color = imag << 24 | (fadeColor & 0xffffff); + if (lp.dimPaint == null) { + lp.dimPaint = new Paint(); + } + lp.dimPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER)); + if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_HARDWARE) { + ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_HARDWARE, lp.dimPaint); + } + invalidateChildRegion(v); + } else if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_NONE) { + if (lp.dimPaint != null) { + lp.dimPaint.setColorFilter(null); + } + final DisableLayerRunnable dlr = new DisableLayerRunnable(v); + mPostedRunnables.add(dlr); + ViewCompat.postOnAnimation(this, dlr); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + boolean result; + final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); + + if (mCanSlide && !lp.slideable && mSlideableView != null) { + // Clip against the slider; no sense drawing what will immediately be covered. + canvas.getClipBounds(mTmpRect); + if (isLayoutRtlSupport()) { + mTmpRect.left = Math.max(mTmpRect.left, mSlideableView.getRight()); + } else { + mTmpRect.right = Math.min(mTmpRect.right, mSlideableView.getLeft()); + } + canvas.clipRect(mTmpRect); + } + + if (Build.VERSION.SDK_INT >= 11) { // HC + result = super.drawChild(canvas, child, drawingTime); + } else { + if (lp.dimWhenOffset && mSlideOffset > 0) { + if (!child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(true); + } + final Bitmap cache = child.getDrawingCache(); + if (cache != null) { + canvas.drawBitmap(cache, child.getLeft(), child.getTop(), lp.dimPaint); + result = false; + } else { + Log.e(TAG, "drawChild: child view " + child + " returned null drawing cache"); + result = super.drawChild(canvas, child, drawingTime); + } + } else { + if (child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(false); + } + result = super.drawChild(canvas, child, drawingTime); + } + } + + canvas.restoreToCount(save); + + return result; + } + + private void invalidateChildRegion(View v) { + IMPL.invalidateChildRegion(this, v); + } + + /** + * Smoothly animate mDraggingPane to the target X position within its range. + * + * @param slideOffset position to animate to + * @param velocity initial velocity in case of fling, or 0. + */ + boolean smoothSlideTo(float slideOffset, int velocity) { + if (!mCanSlide) { + // Nothing to do. + return false; + } + + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + int x; + if (isLayoutRtl) { + int startBound = getPaddingRight() + lp.rightMargin; + int childWidth = mSlideableView.getWidth(); + x = (int) (getWidth() - (startBound + slideOffset * mSlideRange + childWidth)); + } else { + int startBound = getPaddingLeft() + lp.leftMargin; + x = (int) (startBound + slideOffset * mSlideRange); + } + + if (mDragHelper.smoothSlideViewTo(mSlideableView, x, mSlideableView.getTop())) { + setAllChildrenVisible(); + ViewCompat.postInvalidateOnAnimation(this); + return true; + } + return false; + } + + @Override + public void computeScroll() { + if (mDragHelper.continueSettling(true)) { + if (!mCanSlide) { + mDragHelper.abort(); + return; + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + /** + * @deprecated Renamed to {@link #setShadowDrawableLeft(Drawable d)} to support LTR (left to + * right language) and {@link #setShadowDrawableRight(Drawable d)} to support RTL (right to left + * language) during opening/closing. + * + * @param d drawable to use as a shadow + */ + @Deprecated + public void setShadowDrawable(Drawable d) { + setShadowDrawableLeft(d); + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param d drawable to use as a shadow + */ + public void setShadowDrawableLeft(Drawable d) { + mShadowDrawableLeft = d; + } + + /** + * Set a drawable to use as a shadow cast by the left pane onto the right pane + * during opening/closing to support right to left language. + * + * @param d drawable to use as a shadow + */ + public void setShadowDrawableRight(Drawable d) { + mShadowDrawableRight = d; + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param resId Resource ID of a drawable to use + */ + @Deprecated + public void setShadowResource(@DrawableRes int resId) { + setShadowDrawable(getResources().getDrawable(resId)); + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param resId Resource ID of a drawable to use + */ + public void setShadowResourceLeft(int resId) { + setShadowDrawableLeft(getResources().getDrawable(resId)); + } + + /** + * Set a drawable to use as a shadow cast by the left pane onto the right pane + * during opening/closing to support right to left language. + * + * @param resId Resource ID of a drawable to use + */ + public void setShadowResourceRight(int resId) { + setShadowDrawableRight(getResources().getDrawable(resId)); + } + + + @Override + public void draw(Canvas c) { + super.draw(c); + final boolean isLayoutRtl = isLayoutRtlSupport(); + Drawable shadowDrawable; + if (isLayoutRtl) { + shadowDrawable = mShadowDrawableRight; + } else { + shadowDrawable = mShadowDrawableLeft; + } + + final View shadowView = getChildCount() > 1 ? getChildAt(1) : null; + if (shadowView == null || shadowDrawable == null) { + // No need to draw a shadow if we don't have one. + return; + } + + final int top = shadowView.getTop(); + final int bottom = shadowView.getBottom(); + + final int shadowWidth = shadowDrawable.getIntrinsicWidth(); + final int left; + final int right; + if (isLayoutRtlSupport()) { + left = shadowView.getRight(); + right = left + shadowWidth; + } else { + right = shadowView.getLeft(); + left = right - shadowWidth; + } + + shadowDrawable.setBounds(left, top, right, bottom); + shadowDrawable.draw(c); + } + + private void parallaxOtherViews(float slideOffset) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams(); + final boolean dimViews = slideLp.dimWhenOffset && + (isLayoutRtl ? slideLp.rightMargin : slideLp.leftMargin) <= 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == mSlideableView) continue; + + final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy); + mParallaxOffset = slideOffset; + final int newOffset = (int) ((1 - slideOffset) * mParallaxBy); + final int dx = oldOffset - newOffset; + + v.offsetLeftAndRight(isLayoutRtl ? -dx : dx); + + if (dimViews) { + dimChildView(v, isLayoutRtl ? mParallaxOffset - 1 : + 1 - mParallaxOffset, mCoveredFadeColor); + } + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && ViewCompat.canScrollHorizontally(v, (isLayoutRtlSupport() ? dx : -dx)); + } + + boolean isDimmed(View child) { + if (child == null) { + return false; + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return mCanSlide && lp.dimWhenOffset && mSlideOffset > 0; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.isOpen = isSlideable() ? isOpen() : mPreservedOpenState; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.isOpen) { + openPane(); + } else { + closePane(); + } + mPreservedOpenState = ss.isOpen; + } + + private class DragHelperCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + if (mIsUnableToDrag) { + return false; + } + + return ((LayoutParams) child.getLayoutParams()).slideable; + } + + @Override + public void onViewDragStateChanged(int state) { + if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { + if (mSlideOffset == 0) { + updateObscuredViewsVisibility(mSlideableView); + dispatchOnPanelClosed(mSlideableView); + mPreservedOpenState = false; + } else { + dispatchOnPanelOpened(mSlideableView); + mPreservedOpenState = true; + } + } + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + // Make all child views visible in preparation for sliding things around + setAllChildrenVisible(); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + onPanelDragged(left); + invalidate(); + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); + + int left; + if (isLayoutRtlSupport()) { + int startToRight = getPaddingRight() + lp.rightMargin; + if (xvel < 0 || (xvel == 0 && mSlideOffset > 0.5f)) { + startToRight += mSlideRange; + } + int childWidth = mSlideableView.getWidth(); + left = getWidth() - startToRight - childWidth; + } else { + left = getPaddingLeft() + lp.leftMargin; + if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) { + left += mSlideRange; + } + } + mDragHelper.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public int getViewHorizontalDragRange(View child) { + return mSlideRange; + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + final int newLeft; + if (isLayoutRtlSupport()) { + int startBound = getWidth() - + (getPaddingRight() + lp.rightMargin + mSlideableView.getWidth()); + int endBound = startBound - mSlideRange; + newLeft = Math.max(Math.min(left, startBound), endBound); + } else { + int startBound = getPaddingLeft() + lp.leftMargin; + int endBound = startBound + mSlideRange; + newLeft = Math.min(Math.max(left, startBound), endBound); + } + return newLeft; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + // Make sure we never move views vertically. + // This could happen if the child has less height than its parent. + return child.getTop(); + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + mDragHelper.captureChildView(mSlideableView, pointerId); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + private static final int[] ATTRS = new int[] { + android.R.attr.layout_weight + }; + + /** + * The weighted proportion of how much of the leftover space + * this child should consume after measurement. + */ + public float weight = 0; + + /** + * True if this pane is the slideable pane in the layout. + */ + boolean slideable; + + /** + * True if this view should be drawn dimmed + * when it's been offset from its default position. + */ + boolean dimWhenOffset; + + Paint dimPaint; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + this.weight = source.weight; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); + this.weight = a.getFloat(0, 0); + a.recycle(); + } + + } + + static class SavedState extends BaseSavedState { + boolean isOpen; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + isOpen = in.readInt() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(isOpen ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + interface SlidingPanelLayoutImpl { + void invalidateChildRegion(SlidingPaneLayout parent, View child); + } + + static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), + child.getRight(), child.getBottom()); + } + } + + static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { + /* + * Private API hacks! Nasty! Bad! + * + * In Jellybean, some optimizations in the hardware UI renderer + * prevent a changed Paint on a View using a hardware layer from having + * the intended effect. This twiddles some internal bits on the view to force + * it to recreate the display list. + */ + private Method mGetDisplayList; + private Field mRecreateDisplayList; + + SlidingPanelLayoutImplJB() { + try { + mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e); + } + try { + mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); + mRecreateDisplayList.setAccessible(true); + } catch (NoSuchFieldException e) { + Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e); + } + } + + @Override + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + if (mGetDisplayList != null && mRecreateDisplayList != null) { + try { + mRecreateDisplayList.setBoolean(child, true); + mGetDisplayList.invoke(child, (Object[]) null); + } catch (Exception e) { + Log.e(TAG, "Error refreshing display list state", e); + } + } else { + // Slow path. REALLY slow path. Let's hope we don't get here. + child.invalidate(); + return; + } + super.invalidateChildRegion(parent, child); + } + } + + static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase { + @Override + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint); + } + } + + class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + copyNodeInfoNoChildren(info, superNode); + superNode.recycle(); + + info.setClassName(SlidingPaneLayout.class.getName()); + info.setSource(host); + + final ViewParent parent = ViewCompat.getParentForAccessibility(host); + if (parent instanceof View) { + info.setParent((View) parent); + } + + // This is a best-approximation of addChildrenForAccessibility() + // that accounts for filtering. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!filter(child) && (child.getVisibility() == View.VISIBLE)) { + // Force importance to "yes" since we can't read the value. + ViewCompat.setImportantForAccessibility( + child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + info.addChild(child); + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + + event.setClassName(SlidingPaneLayout.class.getName()); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (!filter(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + public boolean filter(View child) { + return isDimmed(child); + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + + dest.setMovementGranularities(src.getMovementGranularities()); + } + } + + private class DisableLayerRunnable implements Runnable { + final View mChildView; + + DisableLayerRunnable(View childView) { + mChildView = childView; + } + + @Override + public void run() { + if (mChildView.getParent() == SlidingPaneLayout.this) { + ViewCompat.setLayerType(mChildView, ViewCompat.LAYER_TYPE_NONE, null); + invalidateChildRegion(mChildView); + } + mPostedRunnables.remove(this); + } + } + + private boolean isLayoutRtlSupport() { + return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java b/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java new file mode 100644 index 00000000000..48574799881 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java @@ -0,0 +1,84 @@ +/* + * 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.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; + +/** + * Space is a lightweight {@link View} subclass that may be used to create gaps between components + * in general purpose layouts. + */ +public class Space extends View { + + public Space(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (getVisibility() == VISIBLE) { + setVisibility(INVISIBLE); + } + } + + public Space(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Space(Context context) { + this(context, null); + } + + /** + * Draw nothing. + * + * @param canvas an unused parameter. + */ + @Override + public void draw(Canvas canvas) { + } + + /** + * Compare to: {@link View#getDefaultSize(int, int)} + * If mode is AT_MOST, return the child size instead of the parent size + * (unless it is too big). + */ + private static int getDefaultSize2(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + result = Math.min(size, specSize); + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec)); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java new file mode 100644 index 00000000000..2314a5ef2d9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java @@ -0,0 +1,276 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + + +/** + * Custom progress bar that shows a cycle of colors as widening circles that + * overdraw each other. When finished, the bar is cleared from the inside out as + * the main cycle continues. Before running, this can also indicate how close + * the user is to triggering something (e.g. how far they need to pull down to + * trigger a refresh). + */ +final class SwipeProgressBar { + + // Default progress animation colors are grays. + private final static int COLOR1 = 0xB3000000; + private final static int COLOR2 = 0x80000000; + private final static int COLOR3 = 0x4d000000; + private final static int COLOR4 = 0x1a000000; + + // The duration of the animation cycle. + private static final int ANIMATION_DURATION_MS = 2000; + + // The duration of the animation to clear the bar. + private static final int FINISH_ANIMATION_DURATION_MS = 1000; + + // Interpolator for varying the speed of the animation. + private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); + + private final Paint mPaint = new Paint(); + private final RectF mClipRect = new RectF(); + private float mTriggerPercentage; + private long mStartTime; + private long mFinishTime; + private boolean mRunning; + + // Colors used when rendering the animation, + private int mColor1; + private int mColor2; + private int mColor3; + private int mColor4; + private View mParent; + + private Rect mBounds = new Rect(); + + public SwipeProgressBar(View parent) { + mParent = parent; + mColor1 = COLOR1; + mColor2 = COLOR2; + mColor3 = COLOR3; + mColor4 = COLOR4; + } + + /** + * Set the four colors used in the progress animation. The first color will + * also be the color of the bar that grows in response to a user swipe + * gesture. + * + * @param color1 Integer representation of a color. + * @param color2 Integer representation of a color. + * @param color3 Integer representation of a color. + * @param color4 Integer representation of a color. + */ + void setColorScheme(int color1, int color2, int color3, int color4) { + mColor1 = color1; + mColor2 = color2; + mColor3 = color3; + mColor4 = color4; + } + + /** + * Update the progress the user has made toward triggering the swipe + * gesture. and use this value to update the percentage of the trigger that + * is shown. + */ + void setTriggerPercentage(float triggerPercentage) { + mTriggerPercentage = triggerPercentage; + mStartTime = 0; + ViewCompat.postInvalidateOnAnimation( + mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } + + /** + * Start showing the progress animation. + */ + void start() { + if (!mRunning) { + mTriggerPercentage = 0; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mRunning = true; + mParent.postInvalidate(); + } + } + + /** + * Stop showing the progress animation. + */ + void stop() { + if (mRunning) { + mTriggerPercentage = 0; + mFinishTime = AnimationUtils.currentAnimationTimeMillis(); + mRunning = false; + mParent.postInvalidate(); + } + } + + /** + * @return Return whether the progress animation is currently running. + */ + boolean isRunning() { + return mRunning || mFinishTime > 0; + } + + void draw(Canvas canvas) { + final int width = mBounds.width(); + final int height = mBounds.height(); + final int cx = width / 2; + final int cy = height / 2; + boolean drawTriggerWhileFinishing = false; + int restoreCount = canvas.save(); + canvas.clipRect(mBounds); + + if (mRunning || (mFinishTime > 0)) { + long now = AnimationUtils.currentAnimationTimeMillis(); + long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; + long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; + float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); + + // If we're not running anymore, that means we're running through + // the finish animation. + if (!mRunning) { + // If the finish animation is done, don't draw anything, and + // don't repost. + if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { + mFinishTime = 0; + return; + } + + // Otherwise, use a 0 opacity alpha layer to clear the animation + // from the inside out. This layer will prevent the circles from + // drawing within its bounds. + long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; + float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); + float pct = (finishProgress / 100f); + // Radius of the circle is half of the screen. + float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); + mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); + canvas.saveLayerAlpha(mClipRect, 0, 0); + // Only draw the trigger if there is a space in the center of + // this refreshing view that needs to be filled in by the + // trigger. If the progress view is just still animating, let it + // continue animating. + drawTriggerWhileFinishing = true; + } + + // First fill in with the last color that would have finished drawing. + if (iterations == 0) { + canvas.drawColor(mColor1); + } else { + if (rawProgress >= 0 && rawProgress < 25) { + canvas.drawColor(mColor4); + } else if (rawProgress >= 25 && rawProgress < 50) { + canvas.drawColor(mColor1); + } else if (rawProgress >= 50 && rawProgress < 75) { + canvas.drawColor(mColor2); + } else { + canvas.drawColor(mColor3); + } + } + + // Then draw up to 4 overlapping concentric circles of varying radii, based on how far + // along we are in the cycle. + // progress 0-50 draw mColor2 + // progress 25-75 draw mColor3 + // progress 50-100 draw mColor4 + // progress 75 (wrap to 25) draw mColor1 + if ((rawProgress >= 0 && rawProgress <= 25)) { + float pct = (((rawProgress + 25) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor1, pct); + } + if (rawProgress >= 0 && rawProgress <= 50) { + float pct = ((rawProgress * 2) / 100f); + drawCircle(canvas, cx, cy, mColor2, pct); + } + if (rawProgress >= 25 && rawProgress <= 75) { + float pct = (((rawProgress - 25) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor3, pct); + } + if (rawProgress >= 50 && rawProgress <= 100) { + float pct = (((rawProgress - 50) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor4, pct); + } + if ((rawProgress >= 75 && rawProgress <= 100)) { + float pct = (((rawProgress - 75) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor1, pct); + } + if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { + // There is some portion of trigger to draw. Restore the canvas, + // then draw the trigger. Otherwise, the trigger does not appear + // until after the bar has finished animating and appears to + // just jump in at a larger width than expected. + canvas.restoreToCount(restoreCount); + restoreCount = canvas.save(); + canvas.clipRect(mBounds); + drawTrigger(canvas, cx, cy); + } + // Keep running until we finish out the last cycle. + ViewCompat.postInvalidateOnAnimation( + mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } else { + // Otherwise if we're in the middle of a trigger, draw that. + if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { + drawTrigger(canvas, cx, cy); + } + } + canvas.restoreToCount(restoreCount); + } + + private void drawTrigger(Canvas canvas, int cx, int cy) { + mPaint.setColor(mColor1); + canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); + } + + /** + * Draws a circle centered in the view. + * + * @param canvas the canvas to draw on + * @param cx the center x coordinate + * @param cy the center y coordinate + * @param color the color to draw + * @param pct the percentage of the view that the circle should cover + */ + private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { + mPaint.setColor(color); + canvas.save(); + canvas.translate(cx, cy); + float radiusScale = INTERPOLATOR.getInterpolation(pct); + canvas.scale(radiusScale, radiusScale); + canvas.drawCircle(0, 0, cx, mPaint); + canvas.restore(); + } + + /** + * Set the drawing bounds of this SwipeProgressBar. + */ + void setBounds(int left, int top, int right, int bottom) { + mBounds.left = left; + mBounds.top = top; + mBounds.right = right; + mBounds.bottom = bottom; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java new file mode 100644 index 00000000000..55f2ce2b1bc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java @@ -0,0 +1,956 @@ +/* + * 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. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ScrollingView; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.widget.AbsListView; + +/** + * The SwipeRefreshLayout should be used whenever the user can refresh the + * contents of a view via a vertical swipe gesture. The activity that + * instantiates this view should add an OnRefreshListener to be notified + * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout + * will notify the listener each and every time the gesture is completed again; + * the listener is responsible for correctly determining when to actually + * initiate a refresh of its content. If the listener determines there should + * not be a refresh, it must call setRefreshing(false) to cancel any visual + * indication of a refresh. If an activity wishes to show just the progress + * animation, it should call setRefreshing(true). To disable the gesture and + * progress animation, call setEnabled(false) on the view. + *

    + * This layout should be made the parent of the view that will be refreshed as a + * result of the gesture and can only support one direct child. This view will + * also be made the target of the gesture and will be forced to match both the + * width and the height supplied in this layout. The SwipeRefreshLayout does not + * provide accessibility events; instead, a menu item must be provided to allow + * refresh of the content wherever this gesture is used. + *

    + */ +public class SwipeRefreshLayout extends ViewGroup { + // Maps to ProgressBar.Large style + public static final int LARGE = MaterialProgressDrawable.LARGE; + // Maps to ProgressBar default style + public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; + + private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); + + private static final int MAX_ALPHA = 255; + private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); + + private static final int CIRCLE_DIAMETER = 40; + private static final int CIRCLE_DIAMETER_LARGE = 56; + + private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; + private static final int INVALID_POINTER = -1; + private static final float DRAG_RATE = .5f; + + // Max amount of circle that can be filled by progress during swipe gesture, + // where 1.0 is a full circle + private static final float MAX_PROGRESS_ANGLE = .8f; + + private static final int SCALE_DOWN_DURATION = 150; + + private static final int ALPHA_ANIMATION_DURATION = 300; + + private static final int ANIMATE_TO_TRIGGER_DURATION = 200; + + private static final int ANIMATE_TO_START_DURATION = 200; + + // Default background for the progress spinner + private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; + // Default offset in dips from the top of the view to where the progress spinner should stop + private static final int DEFAULT_CIRCLE_TARGET = 64; + + private View mTarget; // the target of the gesture + private OnRefreshListener mListener; + private boolean mRefreshing = false; + private int mTouchSlop; + private float mTotalDragDistance = -1; + private int mMediumAnimationDuration; + private int mCurrentTargetOffsetTop; + // Whether or not the starting offset has been determined. + private boolean mOriginalOffsetCalculated = false; + + private float mInitialMotionY; + private float mInitialDownY; + private boolean mIsBeingDragged; + private int mActivePointerId = INVALID_POINTER; + // Whether this item is scaled up rather than clipped + private boolean mScale; + + // Target is returning to its start offset because it was cancelled or a + // refresh was triggered. + private boolean mReturningToStart; + private final DecelerateInterpolator mDecelerateInterpolator; + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.enabled + }; + + private CircleImageView mCircleView; + private int mCircleViewIndex = -1; + + protected int mFrom; + + private float mStartingScale; + + protected int mOriginalOffsetTop; + + private MaterialProgressDrawable mProgress; + + private Animation mScaleAnimation; + + private Animation mScaleDownAnimation; + + private Animation mAlphaStartAnimation; + + private Animation mAlphaMaxAnimation; + + private Animation mScaleDownToStartAnimation; + + private float mSpinnerFinalOffset; + + private boolean mNotify; + + private int mCircleWidth; + + private int mCircleHeight; + + // Whether the client has set a custom starting position; + private boolean mUsingCustomStart; + + private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if (mRefreshing) { + // Make sure the progress view is fully visible + mProgress.setAlpha(MAX_ALPHA); + mProgress.start(); + if (mNotify) { + if (mListener != null) { + mListener.onRefresh(); + } + } + } else { + mProgress.stop(); + mCircleView.setVisibility(View.GONE); + setColorViewAlpha(MAX_ALPHA); + // Return the circle to its start position + if (mScale) { + setAnimationProgress(0 /* animation complete and view is hidden */); + } else { + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, + true /* requires update */); + } + } + mCurrentTargetOffsetTop = mCircleView.getTop(); + } + }; + + private void setColorViewAlpha(int targetAlpha) { + mCircleView.getBackground().setAlpha(targetAlpha); + mProgress.setAlpha(targetAlpha); + } + + /** + * The refresh indicator starting and resting position is always positioned + * near the top of the refreshing content. This position is a consistent + * location, but can be adjusted in either direction based on whether or not + * there is a toolbar or actionbar present. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param start The offset in pixels from the top of this view at which the + * progress spinner should appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + public void setProgressViewOffset(boolean scale, int start, int end) { + mScale = scale; + mCircleView.setVisibility(View.GONE); + mOriginalOffsetTop = mCurrentTargetOffsetTop = start; + mSpinnerFinalOffset = end; + mUsingCustomStart = true; + mCircleView.invalidate(); + } + + /** + * The refresh indicator resting position is always positioned near the top + * of the refreshing content. This position is a consistent location, but + * can be adjusted in either direction based on whether or not there is a + * toolbar or actionbar present. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + public void setProgressViewEndTarget(boolean scale, int end) { + mSpinnerFinalOffset = end; + mScale = scale; + mCircleView.invalidate(); + } + + /** + * One of DEFAULT, or LARGE. + */ + public void setSize(int size) { + if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { + return; + } + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + if (size == MaterialProgressDrawable.LARGE) { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); + } else { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + } + // force the bounds of the progress circle inside the circle view to + // update by setting it to null before updating its size and then + // re-setting it + mCircleView.setImageDrawable(null); + mProgress.updateSizes(size); + mCircleView.setImageDrawable(mProgress); + } + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * + * @param context + */ + public SwipeRefreshLayout(Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * + * @param context + * @param attrs + */ + public SwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); + + createProgressView(); + ViewCompat.setChildrenDrawingOrderEnabled(this, true); + // the absolute offset has to take into account that the circle starts at an offset + mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; + mTotalDragDistance = mSpinnerFinalOffset; + } + + protected int getChildDrawingOrder(int childCount, int i) { + if (mCircleViewIndex < 0) { + return i; + } else if (i == childCount - 1) { + // Draw the selected child last + return mCircleViewIndex; + } else if (i >= mCircleViewIndex) { + // Move the children after the selected child earlier one + return i + 1; + } else { + // Keep the children before the selected child the same + return i; + } + } + + private void createProgressView() { + mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); + mProgress = new MaterialProgressDrawable(getContext(), this); + mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); + mCircleView.setImageDrawable(mProgress); + mCircleView.setVisibility(View.GONE); + addView(mCircleView); + } + + /** + * Set the listener to be notified when a refresh is triggered via the swipe + * gesture. + */ + public void setOnRefreshListener(OnRefreshListener listener) { + mListener = listener; + } + + /** + * Pre API 11, alpha is used to make the progress circle appear instead of scale. + */ + private boolean isAlphaUsedForScale() { + return android.os.Build.VERSION.SDK_INT < 11; + } + + /** + * Notify the widget that refresh state has changed. Do not call this when + * refresh is triggered by a swipe gesture. + * + * @param refreshing Whether or not the view should show refresh progress. + */ + public void setRefreshing(boolean refreshing) { + if (refreshing && mRefreshing != refreshing) { + // scale and show + mRefreshing = refreshing; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); + } else { + endTarget = (int) mSpinnerFinalOffset; + } + setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, + true /* requires update */); + mNotify = false; + startScaleUpAnimation(mRefreshListener); + } else { + setRefreshing(refreshing, false /* notify */); + } + } + + private void startScaleUpAnimation(AnimationListener listener) { + mCircleView.setVisibility(View.VISIBLE); + if (android.os.Build.VERSION.SDK_INT >= 11) { + // Pre API 11, alpha is used in place of scale up to show the + // progress circle appearing. + // Don't adjust the alpha during appearance otherwise. + mProgress.setAlpha(MAX_ALPHA); + } + mScaleAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(interpolatedTime); + } + }; + mScaleAnimation.setDuration(mMediumAnimationDuration); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleAnimation); + } + + /** + * Pre API 11, this does an alpha animation. + * @param progress + */ + private void setAnimationProgress(float progress) { + if (isAlphaUsedForScale()) { + setColorViewAlpha((int) (progress * MAX_ALPHA)); + } else { + ViewCompat.setScaleX(mCircleView, progress); + ViewCompat.setScaleY(mCircleView, progress); + } + } + + private void setRefreshing(boolean refreshing, final boolean notify) { + if (mRefreshing != refreshing) { + mNotify = notify; + ensureTarget(); + mRefreshing = refreshing; + if (mRefreshing) { + animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); + } else { + startScaleDownAnimation(mRefreshListener); + } + } + } + + private void startScaleDownAnimation(Animation.AnimationListener listener) { + mScaleDownAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(1 - interpolatedTime); + } + }; + mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); + mCircleView.setAnimationListener(listener); + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleDownAnimation); + } + + private void startProgressAlphaStartAnimation() { + mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); + } + + private void startProgressAlphaMaxAnimation() { + mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); + } + + private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { + // Pre API 11, alpha is used in place of scale. Don't also use it to + // show the trigger point. + if (mScale && isAlphaUsedForScale()) { + return null; + } + Animation alpha = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + mProgress + .setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) + * interpolatedTime))); + } + }; + alpha.setDuration(ALPHA_ANIMATION_DURATION); + // Clear out the previous animation listeners. + mCircleView.setAnimationListener(null); + mCircleView.clearAnimation(); + mCircleView.startAnimation(alpha); + return alpha; + } + + /** + * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)} + */ + @Deprecated + public void setProgressBackgroundColor(int colorRes) { + setProgressBackgroundColorSchemeResource(colorRes); + } + + /** + * Set the background color of the progress spinner disc. + * + * @param colorRes Resource id of the color. + */ + public void setProgressBackgroundColorSchemeResource(int colorRes) { + setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes)); + } + + /** + * Set the background color of the progress spinner disc. + * + * @param color + */ + public void setProgressBackgroundColorSchemeColor(int color) { + mCircleView.setBackgroundColor(color); + mProgress.setBackgroundColor(color); + } + + /** + * @deprecated Use {@link #setColorSchemeResources(int...)} + */ + @Deprecated + public void setColorScheme(int... colors) { + setColorSchemeResources(colors); + } + + /** + * Set the color resources used in the progress animation from color resources. + * The first color will also be the color of the bar that grows in response + * to a user swipe gesture. + * + * @param colorResIds + */ + public void setColorSchemeResources(int... colorResIds) { + final Resources res = getResources(); + int[] colorRes = new int[colorResIds.length]; + for (int i = 0; i < colorResIds.length; i++) { + colorRes[i] = res.getColor(colorResIds[i]); + } + setColorSchemeColors(colorRes); + } + + /** + * Set the colors used in the progress animation. The first + * color will also be the color of the bar that grows in response to a user + * swipe gesture. + * + * @param colors + */ + public void setColorSchemeColors(int... colors) { + ensureTarget(); + mProgress.setColorSchemeColors(colors); + } + + /** + * @return Whether the SwipeRefreshWidget is actively showing refresh + * progress. + */ + public boolean isRefreshing() { + return mRefreshing; + } + + private void ensureTarget() { + // Don't bother getting the parent height if the parent hasn't been laid + // out yet. + if (mTarget == null) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (!child.equals(mCircleView)) { + mTarget = child; + break; + } + } + } + } + + /** + * Set the distance to trigger a sync in dips + * + * @param distance + */ + public void setDistanceToTriggerSync(int distance) { + mTotalDragDistance = distance; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int width = getMeasuredWidth(); + final int height = getMeasuredHeight(); + if (getChildCount() == 0) { + return; + } + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + final View child = mTarget; + final int childLeft = getPaddingLeft(); + final int childTop = getPaddingTop(); + final int childWidth = width - getPaddingLeft() - getPaddingRight(); + final int childHeight = height - getPaddingTop() - getPaddingBottom(); + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + int circleWidth = mCircleView.getMeasuredWidth(); + int circleHeight = mCircleView.getMeasuredHeight(); + mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, + (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + mTarget.measure(MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( + getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); + mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); + if (!mUsingCustomStart && !mOriginalOffsetCalculated) { + mOriginalOffsetCalculated = true; + mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + } + mCircleViewIndex = -1; + // Get the index of the circleview. + for (int index = 0; index < getChildCount(); index++) { + if (getChildAt(index) == mCircleView) { + mCircleViewIndex = index; + break; + } + } + } + + /** + * Get the diameter of the progress circle that is displayed as part of the + * swipe to refresh layout. This is not valid until a measure pass has + * completed. + * + * @return Diameter in pixels of the progress circle view. + */ + public int getProgressCircleDiameter() { + return mCircleView != null ?mCircleView.getMeasuredHeight() : 0; + } + + /** + * @return Whether it is possible for the child view of this layout to + * scroll up. Override this if the child view is a custom view. + */ + public boolean canChildScrollUp() { + if (android.os.Build.VERSION.SDK_INT < 14) { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + return absListView.getChildCount() > 0 + && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) + .getTop() < absListView.getPaddingTop()); + } else { + return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0; + } + } else { + return ViewCompat.canScrollVertically(mTarget, -1); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + final float initialDownY = getMotionEventY(ev, mActivePointerId); + if (initialDownY == -1) { + return false; + } + mInitialDownY = initialDownY; + break; + + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); + return false; + } + + final float y = getMotionEventY(ev, mActivePointerId); + if (y == -1) { + return false; + } + final float yDiff = y - mInitialDownY; + if (yDiff > mTouchSlop && !mIsBeingDragged) { + mInitialMotionY = mInitialDownY + mTouchSlop; + mIsBeingDragged = true; + mProgress.setAlpha(STARTING_PROGRESS_ALPHA); + } + break; + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + break; + } + + return mIsBeingDragged; + } + + private float getMotionEventY(MotionEvent ev, int activePointerId) { + final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (index < 0) { + return -1; + } + return MotionEventCompat.getY(ev, index); + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // Nope. + } + + private boolean isAnimationRunning(Animation animation) { + return animation != null && animation.hasStarted() && !animation.hasEnded(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp()) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + break; + + case MotionEvent.ACTION_MOVE: { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (pointerIndex < 0) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); + return false; + } + + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + if (mIsBeingDragged) { + mProgress.showArrow(true); + float originalDragPercent = overscrollTop / mTotalDragDistance; + if (originalDragPercent < 0) { + return false; + } + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; + float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; + float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset + - mOriginalOffsetTop : mSpinnerFinalOffset; + float tensionSlingshotPercent = Math.max(0, + Math.min(extraOS, slingshotDist * 2) / slingshotDist); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float extraMove = (slingshotDist) * tensionPercent * 2; + + int targetY = mOriginalOffsetTop + + (int) ((slingshotDist * dragPercent) + extraMove); + // where 1.0f is a full circle + if (mCircleView.getVisibility() != View.VISIBLE) { + mCircleView.setVisibility(View.VISIBLE); + } + if (!mScale) { + ViewCompat.setScaleX(mCircleView, 1f); + ViewCompat.setScaleY(mCircleView, 1f); + } + if (overscrollTop < mTotalDragDistance) { + if (mScale) { + setAnimationProgress(overscrollTop / mTotalDragDistance); + } + if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA + && !isAnimationRunning(mAlphaStartAnimation)) { + // Animate the alpha + startProgressAlphaStartAnimation(); + } + float strokeStart = adjustedPercent * .8f; + mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); + mProgress.setArrowScale(Math.min(1f, adjustedPercent)); + } else { + if (mProgress.getAlpha() < MAX_ALPHA + && !isAnimationRunning(mAlphaMaxAnimation)) { + // Animate the alpha + startProgressAlphaMaxAnimation(); + } + } + float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; + mProgress.setProgressRotation(rotation); + setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, + true /* requires update */); + } + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mActivePointerId == INVALID_POINTER) { + if (action == MotionEvent.ACTION_UP) { + Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); + } + return false; + } + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + mIsBeingDragged = false; + if (overscrollTop > mTotalDragDistance) { + setRefreshing(true, true /* notify */); + } else { + // cancel refresh + mRefreshing = false; + mProgress.setStartEndTrim(0f, 0f); + Animation.AnimationListener listener = null; + if (!mScale) { + listener = new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if (!mScale) { + startScaleDownAnimation(null); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + }; + } + animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); + mProgress.showArrow(false); + } + mActivePointerId = INVALID_POINTER; + return false; + } + } + + return true; + } + + private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToCorrectPosition.reset(); + mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); + mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mAnimateToCorrectPosition); + } + + private void animateOffsetToStartPosition(int from, AnimationListener listener) { + if (mScale) { + // Scale the item back down + startScaleDownReturnToStartAnimation(from, listener); + } else { + mFrom = from; + mAnimateToStartPosition.reset(); + mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); + mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mAnimateToStartPosition); + } + } + + private final Animation mAnimateToCorrectPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + int targetTop = 0; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); + } else { + endTarget = (int) mSpinnerFinalOffset; + } + targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + mProgress.setArrowScale(1 - interpolatedTime); + } + }; + + private void moveToStart(float interpolatedTime) { + int targetTop = 0; + targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + } + + private final Animation mAnimateToStartPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + moveToStart(interpolatedTime); + } + }; + + private void startScaleDownReturnToStartAnimation(int from, + Animation.AnimationListener listener) { + mFrom = from; + if (isAlphaUsedForScale()) { + mStartingScale = mProgress.getAlpha(); + } else { + mStartingScale = ViewCompat.getScaleX(mCircleView); + } + mScaleDownToStartAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); + setAnimationProgress(targetScale); + moveToStart(interpolatedTime); + } + }; + mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleDownToStartAnimation); + } + + private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { + mCircleView.bringToFront(); + mCircleView.offsetTopAndBottom(offset); + mCurrentTargetOffsetTop = mCircleView.getTop(); + if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { + invalidate(); + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + public void onRefresh(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java new file mode 100644 index 00000000000..c31196aeac1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java @@ -0,0 +1,203 @@ +/* + * 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.v4.widget; + +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +/** + * Helper for accessing features in {@link TextView} introduced after API level + * 4 in a backwards compatible fashion. + */ +public class TextViewCompat { + + // Hide constructor + private TextViewCompat() { + } + + interface TextViewCompatImpl { + + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom); + + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom); + + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom); + + } + + static class BaseTextViewCompatImpl implements TextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawables(start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + } + + static class JbMr1TextViewCompatImpl extends BaseTextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + + } + + static class JbMr2TextViewCompatImpl extends JbMr1TextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr2.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr2 + .setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, + bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + TextViewCompatJbMr2.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + } + + static final TextViewCompatImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 18) { + IMPL = new JbMr2TextViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1TextViewCompatImpl(); + } else { + IMPL = new BaseTextViewCompatImpl(); + } + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables must already have had {@link Drawable#setBounds} + * called. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + IMPL.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables' bounds will be set to their intrinsic bounds. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use 0 if you do not want a Drawable there. The + * Drawables' bounds will be set to their intrinsic bounds. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @param start Resource identifier of the start Drawable. + * @param top Resource identifier of the top Drawable. + * @param end Resource identifier of the end Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java new file mode 100644 index 00000000000..7c073c65e12 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java @@ -0,0 +1,49 @@ +/* + * 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.v4.widget; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.widget.TextView; + +class TextViewCompatJbMr1 { + + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, rtl ? start : end, + bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, rtl ? start : end, + bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java new file mode 100644 index 00000000000..ec9fe613bff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java @@ -0,0 +1,43 @@ +/* + * 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.v4.widget; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +class TextViewCompatJbMr2 { + + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesRelative(start, top, end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java new file mode 100644 index 00000000000..3f4e571276f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java @@ -0,0 +1,1482 @@ +/* + * 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. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import java.util.Arrays; + +/** + * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number + * of useful operations and state tracking for allowing a user to drag and reposition + * views within their parent ViewGroup. + */ +public class ViewDragHelper { + private static final String TAG = "ViewDragHelper"; + + /** + * A null/invalid pointer ID. + */ + public static final int INVALID_POINTER = -1; + + /** + * A view is not currently being dragged or animating as a result of a fling/snap. + */ + public static final int STATE_IDLE = 0; + + /** + * A view is currently being dragged. The position is currently changing as a result + * of user input or simulated user input. + */ + public static final int STATE_DRAGGING = 1; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = 2; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = 1 << 0; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = 1 << 1; + + /** + * Edge flag indicating that the top edge should be affected. + */ + public static final int EDGE_TOP = 1 << 2; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = 1 << 3; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * Indicates that a check should occur along the horizontal axis + */ + public static final int DIRECTION_HORIZONTAL = 1 << 0; + + /** + * Indicates that a check should occur along the vertical axis + */ + public static final int DIRECTION_VERTICAL = 1 << 1; + + /** + * Indicates that a check should occur along all axes + */ + public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + private static final int EDGE_SIZE = 20; // dp + + private static final int BASE_SETTLE_DURATION = 256; // ms + private static final int MAX_SETTLE_DURATION = 600; // ms + + // Current drag state; idle, dragging or settling + private int mDragState; + + // Distance to travel before a drag may begin + private int mTouchSlop; + + // Last known position/pointer tracking + private int mActivePointerId = INVALID_POINTER; + private float[] mInitialMotionX; + private float[] mInitialMotionY; + private float[] mLastMotionX; + private float[] mLastMotionY; + private int[] mInitialEdgesTouched; + private int[] mEdgeDragsInProgress; + private int[] mEdgeDragsLocked; + private int mPointersDown; + + private VelocityTracker mVelocityTracker; + private float mMaxVelocity; + private float mMinVelocity; + + private int mEdgeSize; + private int mTrackingEdges; + + private ScrollerCompat mScroller; + + private final Callback mCallback; + + private View mCapturedView; + private boolean mReleaseInProgress; + + private final ViewGroup mParentView; + + /** + * A Callback is used as a communication channel with the ViewDragHelper back to the + * parent view using it. on*methods are invoked on siginficant events and several + * accessor methods are expected to provide the ViewDragHelper with more information + * about the state of the parent view upon request. The callback also makes decisions + * governing the range and draggability of child views. + */ + public static abstract class Callback { + /** + * Called when the drag state changes. See the STATE_* constants + * for more information. + * + * @param state The new drag state + * + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onViewDragStateChanged(int state) {} + + /** + * Called when the captured view's position changes as the result of a drag or settle. + * + * @param changedView View whose position changed + * @param left New X coordinate of the left edge of the view + * @param top New Y coordinate of the top edge of the view + * @param dx Change in X position from the last call + * @param dy Change in Y position from the last call + */ + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} + + /** + * Called when a child view is captured for dragging or settling. The ID of the pointer + * currently dragging the captured view is supplied. If activePointerId is + * identified as {@link #INVALID_POINTER} the capture is programmatic instead of + * pointer-initiated. + * + * @param capturedChild Child view that was captured + * @param activePointerId Pointer id tracking the child capture + */ + public void onViewCaptured(View capturedChild, int activePointerId) {} + + /** + * Called when the child view is no longer being actively dragged. + * The fling velocity is also supplied, if relevant. The velocity values may + * be clamped to system minimums or maximums. + * + *

    Calling code may decide to fling or otherwise release the view to let it + * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes + * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} + * and the view capture will not fully end until it comes to a complete stop. + * If neither of these methods is invoked before onViewReleased returns, + * the view will stop in place and the ViewDragHelper will return to + * {@link #STATE_IDLE}.

    + * + * @param releasedChild The captured child view now being released + * @param xvel X velocity of the pointer as it left the screen in pixels per second. + * @param yvel Y velocity of the pointer as it left the screen in pixels per second. + */ + public void onViewReleased(View releasedChild, float xvel, float yvel) {} + + /** + * Called when one of the subscribed edges in the parent view has been touched + * by the user while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) currently touched + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouched(int edgeFlags, int pointerId) {} + + /** + * Called when the given edge may become locked. This can happen if an edge drag + * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} + * was called. This method should return true to lock this edge or false to leave it + * unlocked. The default behavior is to leave edges unlocked. + * + * @param edgeFlags A combination of edge flags describing the edge(s) locked + * @return true to lock the edge, false to leave it unlocked + */ + public boolean onEdgeLock(int edgeFlags) { + return false; + } + + /** + * Called when the user has started a deliberate drag away from one + * of the subscribed edges in the parent view while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) dragged + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeDragStarted(int edgeFlags, int pointerId) {} + + /** + * Called to determine the Z-order of child views. + * + * @param index the ordered position to query for + * @return index of the view that should be ordered at position index + */ + public int getOrderedChildIndex(int index) { + return index; + } + + /** + * Return the magnitude of a draggable child view's horizontal range of motion in pixels. + * This method should return 0 for views that cannot move horizontally. + * + * @param child Child view to check + * @return range of horizontal motion in pixels + */ + public int getViewHorizontalDragRange(View child) { + return 0; + } + + /** + * Return the magnitude of a draggable child view's vertical range of motion in pixels. + * This method should return 0 for views that cannot move vertically. + * + * @param child Child view to check + * @return range of vertical motion in pixels + */ + public int getViewVerticalDragRange(View child) { + return 0; + } + + /** + * Called when the user's input indicates that they want to capture the given child view + * with the pointer indicated by pointerId. The callback should return true if the user + * is permitted to drag the given view with the indicated pointer. + * + *

    ViewDragHelper may call this method multiple times for the same view even if + * the view is already captured; this indicates that a new pointer is trying to take + * control of the view.

    + * + *

    If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} + * will follow if the capture is successful.

    + * + * @param child Child the user is attempting to capture + * @param pointerId ID of the pointer attempting the capture + * @return true if capture should be allowed, false otherwise + */ + public abstract boolean tryCaptureView(View child, int pointerId); + + /** + * Restrict the motion of the dragged child view along the horizontal axis. + * The default implementation does not allow horizontal motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param left Attempted motion along the X axis + * @param dx Proposed change in position for left + * @return The new clamped position for left + */ + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + /** + * Restrict the motion of the dragged child view along the vertical axis. + * The default implementation does not allow vertical motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param top Attempted motion along the Y axis + * @param dy Proposed change in position for top + * @return The new clamped position for top + */ + public int clampViewPositionVertical(View child, int top, int dy) { + return 0; + } + } + + /** + * Interpolator defining the animation curve for mScroller + */ + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final Runnable mSetIdleRunnable = new Runnable() { + public void run() { + setDragState(STATE_IDLE); + } + }; + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + return new ViewDragHelper(forParent.getContext(), forParent, cb); + } + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param sensitivity Multiplier for how sensitive the helper should be about detecting + * the start of a drag. Larger values are more sensitive. 1.0f is normal. + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + final ViewDragHelper helper = create(forParent, cb); + helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); + return helper; + } + + /** + * Apps should use ViewDragHelper.create() to get a new instance. + * This will allow VDH to use internal compatibility implementations for different + * platform versions. + * + * @param context Context to initialize config-dependent params from + * @param forParent Parent view to monitor + */ + private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + if (forParent == null) { + throw new IllegalArgumentException("Parent view may not be null"); + } + if (cb == null) { + throw new IllegalArgumentException("Callback may not be null"); + } + + mParentView = forParent; + mCallback = cb; + + final ViewConfiguration vc = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); + + mTouchSlop = vc.getScaledTouchSlop(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = ScrollerCompat.create(context, sInterpolator); + } + + /** + * Set the minimum velocity that will be detected as having a magnitude greater than zero + * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. + * + * @param minVel Minimum velocity to detect + */ + public void setMinVelocity(float minVel) { + mMinVelocity = minVel; + } + + /** + * Return the currently configured minimum velocity. Any flings with a magnitude less + * than this value in pixels per second. Callback methods accepting a velocity will receive + * zero as a velocity value if the real detected velocity was below this threshold. + * + * @return the minimum velocity that will be detected + */ + public float getMinVelocity() { + return mMinVelocity; + } + + /** + * Retrieve the current drag state of this helper. This will return one of + * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * @return The current drag state + */ + public int getViewDragState() { + return mDragState; + } + + /** + * Enable edge tracking for the selected edges of the parent view. + * The callback's {@link Callback#onEdgeTouched(int, int)} and + * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked + * for edges for which edge tracking has been enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mTrackingEdges = edgeFlags; + } + + /** + * Return the size of an edge. This is the range in pixels along the edges of this view + * that will actively detect edge touches or drags if edge tracking is enabled. + * + * @return The size of an edge in pixels + * @see #setEdgeTrackingEnabled(int) + */ + public int getEdgeSize() { + return mEdgeSize; + } + + /** + * Capture a specific child view for dragging within the parent. The callback will be notified + * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to + * capture this view. + * + * @param childView Child view to capture + * @param activePointerId ID of the pointer that is dragging the captured child view + */ + public void captureChildView(View childView, int activePointerId) { + if (childView.getParent() != mParentView) { + throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); + } + + mCapturedView = childView; + mActivePointerId = activePointerId; + mCallback.onViewCaptured(childView, activePointerId); + setDragState(STATE_DRAGGING); + } + + /** + * @return The currently captured view, or null if no view has been captured. + */ + public View getCapturedView() { + return mCapturedView; + } + + /** + * @return The ID of the pointer currently dragging the captured view, + * or {@link #INVALID_POINTER}. + */ + public int getActivePointerId() { + return mActivePointerId; + } + + /** + * @return The minimum distance in pixels that the user must travel to initiate a drag + */ + public int getTouchSlop() { + return mTouchSlop; + } + + /** + * The result of a call to this method is equivalent to + * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. + */ + public void cancel() { + mActivePointerId = INVALID_POINTER; + clearMotionHistory(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * {@link #cancel()}, but also abort all motion in progress and snap to the end of any + * animation. + */ + public void abort() { + cancel(); + if (mDragState == STATE_SETTLING) { + final int oldX = mScroller.getCurrX(); + final int oldY = mScroller.getCurrY(); + mScroller.abortAnimation(); + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); + } + setDragState(STATE_IDLE); + } + + /** + * Animate the view child to the given (left, top) position. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + *

    This operation does not count as a capture event, though {@link #getCapturedView()} + * will still report the sliding view while the slide is in progress.

    + * + * @param child Child view to capture and animate + * @param finalLeft Final left position of child + * @param finalTop Final top position of child + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + mCapturedView = child; + mActivePointerId = INVALID_POINTER; + + boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); + if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) { + // If we're in an IDLE state to begin with and aren't moving anywhere, we + // end up having a non-null capturedView with an IDLE dragState + mCapturedView = null; + } + + return continueSliding; + } + + /** + * Settle the captured view at the given (left, top) position. + * The appropriate velocity from prior motion will be taken into account. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + * @param finalLeft Settled left edge position for the captured view + * @param finalTop Settled top edge position for the captured view + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean settleCapturedViewAt(int finalLeft, int finalTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + + "Callback#onViewReleased"); + } + + return forceSettleCapturedViewAt(finalLeft, finalTop, + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); + } + + /** + * Settle the captured view at the given (left, top) position. + * + * @param finalLeft Target left position for the captured view + * @param finalTop Target top position for the captured view + * @param xvel Horizontal velocity + * @param yvel Vertical velocity + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { + final int startLeft = mCapturedView.getLeft(); + final int startTop = mCapturedView.getTop(); + final int dx = finalLeft - startLeft; + final int dy = finalTop - startTop; + + if (dx == 0 && dy == 0) { + // Nothing to do. Send callbacks, be done. + mScroller.abortAnimation(); + setDragState(STATE_IDLE); + return false; + } + + final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); + mScroller.startScroll(startLeft, startTop, dx, dy, duration); + + setDragState(STATE_SETTLING); + return true; + } + + private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { + xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); + yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final int absXVel = Math.abs(xvel); + final int absYVel = Math.abs(yvel); + final int addedVel = absXVel + absYVel; + final int addedDistance = absDx + absDy; + + final float xweight = xvel != 0 ? (float) absXVel / addedVel : + (float) absDx / addedDistance; + final float yweight = yvel != 0 ? (float) absYVel / addedVel : + (float) absDy / addedDistance; + + int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); + int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); + + return (int) (xduration * xweight + yduration * yweight); + } + + private int computeAxisDuration(int delta, int velocity, int motionRange) { + if (delta == 0) { + return 0; + } + + final int width = mParentView.getWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float range = (float) Math.abs(delta) / motionRange; + duration = (int) ((range + 1) * BASE_SETTLE_DURATION); + } + return Math.min(duration, MAX_SETTLE_DURATION); + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private int clampMag(int value, int absMin, int absMax) { + final int absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private float clampMag(float value, float absMin, float absMax) { + final float absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Settle the captured view based on standard free-moving fling behavior. + * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame + * to continue the motion until it returns false. + * + * @param minLeft Minimum X position for the view's left edge + * @param minTop Minimum Y position for the view's top edge + * @param maxLeft Maximum X position for the view's left edge + * @param maxTop Maximum Y position for the view's top edge + */ + public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + + "Callback#onViewReleased"); + } + + mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + minLeft, maxLeft, minTop, maxTop); + + setDragState(STATE_SETTLING); + } + + /** + * Move the captured settling view by the appropriate amount for the current time. + * If continueSettling returns true, the caller should call it again + * on the next frame to continue. + * + * @param deferCallbacks true if state callbacks should be deferred via posted message. + * Set this to true if you are calling this method from + * {@link android.view.View#computeScroll()} or similar methods + * invoked as part of layout or drawing. + * @return true if settle is still in progress + */ + public boolean continueSettling(boolean deferCallbacks) { + if (mDragState == STATE_SETTLING) { + boolean keepGoing = mScroller.computeScrollOffset(); + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + final int dx = x - mCapturedView.getLeft(); + final int dy = y - mCapturedView.getTop(); + + if (dx != 0) { + mCapturedView.offsetLeftAndRight(dx); + } + if (dy != 0) { + mCapturedView.offsetTopAndBottom(dy); + } + + if (dx != 0 || dy != 0) { + mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); + } + + if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { + // Close enough. The interpolator/scroller might think we're still moving + // but the user sure doesn't. + mScroller.abortAnimation(); + keepGoing = false; + } + + if (!keepGoing) { + if (deferCallbacks) { + mParentView.post(mSetIdleRunnable); + } else { + setDragState(STATE_IDLE); + } + } + } + + return mDragState == STATE_SETTLING; + } + + /** + * Like all callback events this must happen on the UI thread, but release + * involves some extra semantics. During a release (mReleaseInProgress) + * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. + */ + private void dispatchViewReleased(float xvel, float yvel) { + mReleaseInProgress = true; + mCallback.onViewReleased(mCapturedView, xvel, yvel); + mReleaseInProgress = false; + + if (mDragState == STATE_DRAGGING) { + // onViewReleased didn't call a method that would have changed this. Go idle. + setDragState(STATE_IDLE); + } + } + + private void clearMotionHistory() { + if (mInitialMotionX == null) { + return; + } + Arrays.fill(mInitialMotionX, 0); + Arrays.fill(mInitialMotionY, 0); + Arrays.fill(mLastMotionX, 0); + Arrays.fill(mLastMotionY, 0); + Arrays.fill(mInitialEdgesTouched, 0); + Arrays.fill(mEdgeDragsInProgress, 0); + Arrays.fill(mEdgeDragsLocked, 0); + mPointersDown = 0; + } + + private void clearMotionHistory(int pointerId) { + if (mInitialMotionX == null) { + return; + } + mInitialMotionX[pointerId] = 0; + mInitialMotionY[pointerId] = 0; + mLastMotionX[pointerId] = 0; + mLastMotionY[pointerId] = 0; + mInitialEdgesTouched[pointerId] = 0; + mEdgeDragsInProgress[pointerId] = 0; + mEdgeDragsLocked[pointerId] = 0; + mPointersDown &= ~(1 << pointerId); + } + + private void ensureMotionHistorySizeForId(int pointerId) { + if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { + float[] imx = new float[pointerId + 1]; + float[] imy = new float[pointerId + 1]; + float[] lmx = new float[pointerId + 1]; + float[] lmy = new float[pointerId + 1]; + int[] iit = new int[pointerId + 1]; + int[] edip = new int[pointerId + 1]; + int[] edl = new int[pointerId + 1]; + + if (mInitialMotionX != null) { + System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); + System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); + System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); + System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); + System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); + System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); + System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); + } + + mInitialMotionX = imx; + mInitialMotionY = imy; + mLastMotionX = lmx; + mLastMotionY = lmy; + mInitialEdgesTouched = iit; + mEdgeDragsInProgress = edip; + mEdgeDragsLocked = edl; + } + } + + private void saveInitialMotion(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); + mPointersDown |= 1 << pointerId; + } + + private void saveLastMotion(MotionEvent ev) { + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + mLastMotionX[pointerId] = x; + mLastMotionY[pointerId] = y; + } + } + + /** + * Check if the given pointer ID represents a pointer that is currently down (to the best + * of the ViewDragHelper's knowledge). + * + *

    The state used to report this information is populated by the methods + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not + * been called for all relevant MotionEvents to track, the information reported + * by this method may be stale or incorrect.

    + * + * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent + * @return true if the pointer with the given ID is still down + */ + public boolean isPointerDown(int pointerId) { + return (mPointersDown & 1 << pointerId) != 0; + } + + void setDragState(int state) { + mParentView.removeCallbacks(mSetIdleRunnable); + if (mDragState != state) { + mDragState = state; + mCallback.onViewDragStateChanged(state); + if (mDragState == STATE_IDLE) { + mCapturedView = null; + } + } + } + + /** + * Attempt to capture the view with the given pointer ID. The callback will be involved. + * This will put us into the "dragging" state. If we've already captured this view with + * this pointer this method will immediately return true without consulting the callback. + * + * @param toCapture View to capture + * @param pointerId Pointer to capture with + * @return true if capture was successful + */ + boolean tryCaptureViewForDrag(View toCapture, int pointerId) { + if (toCapture == mCapturedView && mActivePointerId == pointerId) { + // Already done! + return true; + } + if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { + mActivePointerId = pointerId; + captureChildView(toCapture, pointerId); + return true; + } + return false; + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels along the X axis + * @param dy Delta scrolled in pixels along the Y axis + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || + ViewCompat.canScrollVertically(v, -dy)); + } + + /** + * Check if this event as provided to the parent view's onInterceptTouchEvent should + * cause the parent to intercept the touch event stream. + * + * @param ev MotionEvent provided to onInterceptTouchEvent + * @return true if the parent view should return true from onInterceptTouchEvent + */ + public boolean shouldInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + saveInitialMotion(x, y, pointerId); + + final View toCapture = findTopChildUnder((int) x, (int) y); + + // Catch a settling view if possible. + if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { + tryCaptureViewForDrag(toCapture, pointerId); + } + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (mDragState == STATE_SETTLING) { + // Catch a settling view if possible. + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture == mCapturedView) { + tryCaptureViewForDrag(toCapture, pointerId); + } + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mInitialMotionX == null || mInitialMotionY == null) break; + + // First to cross a touch slop over a draggable view wins. Also report edge drags. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + final View toCapture = findTopChildUnder((int) x, (int) y); + final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy); + if (pastSlop) { + // check the callback's + // getView[Horizontal|Vertical]DragRange methods to know + // if you can move at all along an axis, then see if it + // would clamp to the same value. If you can't move at + // all in every dimension with a nonzero range, bail. + final int oldLeft = toCapture.getLeft(); + final int targetLeft = oldLeft + (int) dx; + final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, + targetLeft, (int) dx); + final int oldTop = toCapture.getTop(); + final int targetTop = oldTop + (int) dy; + final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, + (int) dy); + final int horizontalDragRange = mCallback.getViewHorizontalDragRange( + toCapture); + final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture); + if ((horizontalDragRange == 0 || horizontalDragRange > 0 + && newLeft == oldLeft) && (verticalDragRange == 0 + || verticalDragRange > 0 && newTop == oldTop)) { + break; + } + } + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag + break; + } + + if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + cancel(); + break; + } + } + + return mDragState == STATE_DRAGGING; + } + + /** + * Process a touch event received by the parent view. This method will dispatch callback events + * as needed before returning. The parent view's onTouchEvent implementation should call this. + * + * @param ev The touch event received by the parent view + */ + public void processTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + final View toCapture = findTopChildUnder((int) x, (int) y); + + saveInitialMotion(x, y, pointerId); + + // Since the parent is already directly processing this touch event, + // there is no reason to delay for a slop before dragging. + // Start immediately if possible. + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + // If we're idle we can do anything! Treat it like a normal down event. + + final View toCapture = findTopChildUnder((int) x, (int) y); + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (isCapturedViewUnder((int) x, (int) y)) { + // We're still tracking a captured view. If the same view is under this + // point, we'll swap to controlling it with this pointer instead. + // (This will still work if we're "catching" a settling view.) + + tryCaptureViewForDrag(mCapturedView, pointerId); + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mDragState == STATE_DRAGGING) { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, index); + final float y = MotionEventCompat.getY(ev, index); + final int idx = (int) (x - mLastMotionX[mActivePointerId]); + final int idy = (int) (y - mLastMotionY[mActivePointerId]); + + dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); + + saveLastMotion(ev); + } else { + // Check to see if any pointer is now over a draggable view. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag. + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { + // Try to find another pointer that's still holding on to the captured view. + int newActivePointer = INVALID_POINTER; + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int id = MotionEventCompat.getPointerId(ev, i); + if (id == mActivePointerId) { + // This one's going away, skip. + continue; + } + + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + if (findTopChildUnder((int) x, (int) y) == mCapturedView && + tryCaptureViewForDrag(mCapturedView, id)) { + newActivePointer = mActivePointerId; + break; + } + } + + if (newActivePointer == INVALID_POINTER) { + // We didn't find another pointer still touching the view, release it. + releaseViewForPointerUp(); + } + } + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: { + if (mDragState == STATE_DRAGGING) { + releaseViewForPointerUp(); + } + cancel(); + break; + } + + case MotionEvent.ACTION_CANCEL: { + if (mDragState == STATE_DRAGGING) { + dispatchViewReleased(0, 0); + } + cancel(); + break; + } + } + } + + private void reportNewEdgeDrags(float dx, float dy, int pointerId) { + int dragsStarted = 0; + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { + dragsStarted |= EDGE_LEFT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { + dragsStarted |= EDGE_TOP; + } + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { + dragsStarted |= EDGE_RIGHT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { + dragsStarted |= EDGE_BOTTOM; + } + + if (dragsStarted != 0) { + mEdgeDragsInProgress[pointerId] |= dragsStarted; + mCallback.onEdgeDragStarted(dragsStarted, pointerId); + } + } + + private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { + final float absDelta = Math.abs(delta); + final float absODelta = Math.abs(odelta); + + if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || + (mEdgeDragsLocked[pointerId] & edge) == edge || + (mEdgeDragsInProgress[pointerId] & edge) == edge || + (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { + return false; + } + if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { + mEdgeDragsLocked[pointerId] |= edge; + return false; + } + return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; + } + + /** + * Check if we've crossed a reasonable touch slop for the given child view. + * If the child cannot be dragged along the horizontal or vertical axis, motion + * along that axis will not count toward the slop check. + * + * @param child Child to check + * @param dx Motion since initial position along X axis + * @param dy Motion since initial position along Y axis + * @return true if the touch slop has been crossed + */ + private boolean checkTouchSlop(View child, float dx, float dy) { + if (child == null) { + return false; + } + final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; + final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

    This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

    + * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions) { + final int count = mInitialMotionX.length; + for (int i = 0; i < count; i++) { + if (checkTouchSlop(directions, i)) { + return true; + } + } + return false; + } + + /** + * Check if the specified pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

    This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

    + * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @param pointerId ID of the pointer to slop check as specified by MotionEvent + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions, int pointerId) { + if (!isPointerDown(pointerId)) { + return false; + } + + final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; + final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; + + final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; + final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any of the edges specified were initially touched in the currently active gesture. + * If there is no currently active gesture this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges) { + final int count = mInitialEdgesTouched.length; + for (int i = 0; i < count; i++) { + if (isEdgeTouched(edges, i)) { + return true; + } + } + return false; + } + + /** + * Check if any of the edges specified were initially touched by the pointer with + * the specified ID. If there is no currently active gesture or if there is no pointer with + * the given ID currently down this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges, int pointerId) { + return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; + } + + private void releaseViewForPointerUp() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float xvel = clampMag( + VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + final float yvel = clampMag( + VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + dispatchViewReleased(xvel, yvel); + } + + private void dragTo(int left, int top, int dx, int dy) { + int clampedX = left; + int clampedY = top; + final int oldLeft = mCapturedView.getLeft(); + final int oldTop = mCapturedView.getTop(); + if (dx != 0) { + clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); + mCapturedView.offsetLeftAndRight(clampedX - oldLeft); + } + if (dy != 0) { + clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); + mCapturedView.offsetTopAndBottom(clampedY - oldTop); + } + + if (dx != 0 || dy != 0) { + final int clampedDx = clampedX - oldLeft; + final int clampedDy = clampedY - oldTop; + mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, + clampedDx, clampedDy); + } + } + + /** + * Determine if the currently captured view is under the given point in the + * parent view's coordinate system. If there is no captured view this method + * will return false. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the captured view is under the given point, false otherwise + */ + public boolean isCapturedViewUnder(int x, int y) { + return isViewUnder(mCapturedView, x, y); + } + + /** + * Determine if the supplied view is under the given point in the + * parent view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false otherwise + */ + public boolean isViewUnder(View view, int x, int y) { + if (view == null) { + return false; + } + return x >= view.getLeft() && + x < view.getRight() && + y >= view.getTop() && + y < view.getBottom(); + } + + /** + * Find the topmost child under the given point within the parent view's coordinate system. + * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return The topmost child view under (x, y) or null if none found. + */ + public View findTopChildUnder(int x, int y) { + final int childCount = mParentView.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); + if (x >= child.getLeft() && x < child.getRight() && + y >= child.getTop() && y < child.getBottom()) { + return child; + } + } + return null; + } + + private int getEdgesTouched(int x, int y) { + int result = 0; + + if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; + if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; + if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; + if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; + + return result; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/package.html b/mobile/google-android-support-v4/src/android/support/v4/widget/package.html new file mode 100644 index 00000000000..e2c636d3b05 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/package.html @@ -0,0 +1,8 @@ + + +Support android.widget classes to assist with development of applications for +android API level 4 or later. This includes a complete modern implementation +of {@link android.support.v4.widget.CursorAdapter} and related classes, which +is needed for use with {@link android.support.v4.content.CursorLoader}. + + diff --git a/mobile/google-android-support-v7-appcompat_lib/.classpath b/mobile/google-android-support-v7-appcompat_lib/.classpath index 7bc01d9a9c6..34488603ca2 100644 --- a/mobile/google-android-support-v7-appcompat_lib/.classpath +++ b/mobile/google-android-support-v7-appcompat_lib/.classpath @@ -1,9 +1,11 @@ - - - - - - - - - + + + + + + + + + + + diff --git a/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml b/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml index dac4cb27ed9..58122b1dcff 100644 --- a/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml +++ b/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml @@ -1,5 +1,6 @@ - - + package="android.support.v7.appcompat" > + + + - + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar deleted file mode 100644 index 0d364e7452a..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar deleted file mode 100644 index bd450c73404..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar deleted file mode 100644 index e13b16c73fd..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml old mode 100644 new mode 100755 index da7ee295c99..35e4aa5c79a --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml @@ -17,4 +17,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml old mode 100644 new mode 100755 index c81b39a9b13..c71dc128375 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml @@ -17,4 +17,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml old mode 100644 new mode 100755 index 79d02d44ca6..c9cb7d3116e --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml @@ -27,4 +27,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml old mode 100644 new mode 100755 index 91664da17ee..c7b18b2112a --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml old mode 100644 new mode 100755 index db7e8073a84..535a0924b58 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml old mode 100644 new mode 100755 index 9a23cd2025a..8d28106bd30 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml @@ -24,4 +24,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml old mode 100644 new mode 100755 index 1afa8febc52..7d819d57a37 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml @@ -17,3 +17,4 @@ android:interpolator="@android:anim/decelerate_interpolator" android:fromYDelta="50%p" android:toYDelta="0" android:duration="@android:integer/config_mediumAnimTime"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml old mode 100644 new mode 100755 index ab824f2e4ac..7dd98809ee6 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml old mode 100644 new mode 100755 index b309fe89c64..e11c80c7293 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml old mode 100644 new mode 100755 index e84d1c7fb6e..9fa9e4fb003 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml old mode 100644 new mode 100755 index e0160766e08..61e47c20698 --- a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml old mode 100644 new mode 100755 index 290faf1a0e0..926f2bd8a33 --- a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml old mode 100644 new mode 100755 index 9a7af53d4ec..e5c143ab491 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml @@ -17,3 +17,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml old mode 100644 new mode 100755 index aa53f3577e8..697c8ba807e --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml @@ -17,3 +17,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml old mode 100644 new mode 100755 index 724c2557dad..2d22cdfccb7 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml old mode 100644 new mode 100755 index 7395e680c65..7f4bd596dfe --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml old mode 100644 new mode 100755 index 7d66d02d637..a82fbea64c2 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml old mode 100644 new mode 100755 index 105b643ddb4..169b3cdb588 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml old mode 100644 new mode 100755 index 0631d5d4ca1..9827356a769 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml old mode 100644 new mode 100755 index 6399b1d028f..5566c2a9eb4 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml old mode 100644 new mode 100755 index 87c015a4cd6..e96a7df570a --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml old mode 100644 new mode 100755 index 6153382c7c5..132aef8a1c2 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml old mode 100644 new mode 100755 index 94d71148213..8e95351d602 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 705e0d9b78c..88f1767fa90 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index b69271067ba..d5e1a001134 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index a6b3696e484..2cf413c5c96 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index b2191dad9f4..d890a62a040 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 2a94e6e4ce2..0620439460d Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index 91b0cb8d01f..fe0ec49abce Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png deleted file mode 100644 index 530ca456daa..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png deleted file mode 100644 index b527495ee61..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 0d2274389f3..a854864d7df Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 17fc083dff1..726b1dc59e3 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index c67a5ddcd17..09d10225d8a Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 133de5258bd..f026a41c853 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index ff3b59630cc..c8b9f68939f Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index 2ab970d5168..f7f306db29b Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index e075ab8ddae..6fd5bfef8e8 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 25274ee8a1f..99e68cc0e7f Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml old mode 100644 new mode 100755 index f3894600ba0..012e5243633 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml @@ -20,3 +20,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml old mode 100644 new mode 100755 index 4934a929d6e..68f606bd7f3 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml old mode 100644 new mode 100755 index c50d4b10f00..95d33823e52 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml @@ -30,3 +30,4 @@ android:bottom="@dimen/abc_button_padding_vertical_material" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml old mode 100644 new mode 100755 index 6e9f9cf3741..3444b5c557b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml old mode 100644 new mode 100755 index 9faf60ac616..52655a5ac3b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml @@ -20,4 +20,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml old mode 100644 new mode 100755 index f20add7e4b8..acacfd9e898 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml old mode 100644 new mode 100755 index 41c4a6f842d..f98179fe369 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml @@ -23,4 +23,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml old mode 100644 new mode 100755 index 248b13af946..5411635caf4 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml @@ -23,4 +23,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml old mode 100644 new mode 100755 index 46c4e912003..ab9c90f4d9b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml @@ -27,3 +27,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml old mode 100644 new mode 100755 index 72162c222ee..db7baccc0b2 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml @@ -24,3 +24,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml old mode 100644 new mode 100755 index 1c180b2ee48..c38248fcdc2 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml @@ -24,3 +24,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml old mode 100644 new mode 100755 index 0add58c86ac..bb98c2f3125 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml old mode 100644 new mode 100755 index 0c1d3e67821..4e95440383b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml old mode 100644 new mode 100755 index 1fb5fc4516d..f23ed0abe89 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml @@ -25,3 +25,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml old mode 100644 new mode 100755 index 8d24047229b..329d880a9b4 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml @@ -26,3 +26,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml old mode 100644 new mode 100755 index 535e2da25e5..964686ac9dc --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml @@ -22,3 +22,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml old mode 100644 new mode 100755 index d0f46a80974..315afd2d15c --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml @@ -33,4 +33,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml old mode 100644 new mode 100755 index ee96ec2e7ab..170e9588ce9 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml old mode 100644 new mode 100755 index 1a8de1b69b5..17aacb204de --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml old mode 100644 new mode 100755 index 08873966e43..503fe9e4fa7 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml @@ -20,3 +20,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml deleted file mode 100644 index 757be1c1df7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml old mode 100644 new mode 100755 index 194afb74cb8..c6c180c4d16 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml @@ -32,3 +32,4 @@ android:ellipsize="end" android:visibility="gone" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml old mode 100644 new mode 100755 index f46550a553e..ca4712a5e8c --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml @@ -21,3 +21,4 @@ android:gravity="center_vertical" android:enabled="false"> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml old mode 100644 new mode 100755 index 5c105ab551c..7ce06a015cf --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml @@ -20,4 +20,4 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" style="?attr/actionBarTabBarStyle"> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml old mode 100644 new mode 100755 index 150ea50dc3c..73482e2aac7 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml @@ -28,3 +28,4 @@ android:textAppearance="?attr/actionMenuTextAppearance" android:textColor="?attr/actionMenuTextColor" style="?attr/actionButtonStyle"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml old mode 100644 new mode 100755 index 4918d2fba96..e1a9e0db20c --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml @@ -22,3 +22,4 @@ app:divider="?attr/actionBarDivider" app:dividerPadding="12dip" android:gravity="center_vertical"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml old mode 100644 new mode 100755 index 34b56a1b09b..ffb6a77ec93 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml @@ -23,3 +23,4 @@ android:visibility="gone" android:theme="?attr/actionBarTheme" style="?attr/actionModeStyle"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml old mode 100644 new mode 100755 index dfc4debec48..c52ce7d4ee6 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml @@ -23,4 +23,4 @@ android:src="?attr/actionModeCloseDrawable" style="?attr/actionModeCloseButtonStyle" android:layout_width="wrap_content" - android:layout_height="match_parent" /> \ No newline at end of file + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml old mode 100644 new mode 100755 index 85fd94e37f1..df8895d0ecc --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml @@ -69,3 +69,4 @@
    + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml old mode 100644 new mode 100755 index 887427d8093..29c798bd8b4 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml @@ -49,4 +49,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml old mode 100644 new mode 100755 index 86d59f76e24..a763dbdbf0f --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml @@ -32,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:gravity="center_vertical|start" + android:gravity="center_vertical" android:paddingLeft="?attr/dialogPreferredPadding" android:paddingRight="?attr/dialogPreferredPadding" android:paddingTop="@dimen/abc_dialog_padding_top_material"> @@ -41,9 +41,9 @@ android:id="@android:id/icon" android:layout_width="32dip" android:layout_height="32dip" - android:layout_marginEnd="8dip" android:scaleType="fitCenter" - android:src="@null"/> + android:src="@null" + style="@style/RtlOverlay.Widget.AppCompat.DialogTitle.Icon"/> + android:textAlignment="viewStart" /> @@ -145,4 +145,4 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml old mode 100644 new mode 100755 index 068b9e907c1..472faacbb66 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml @@ -44,4 +44,4 @@ enabled. android:layout_height="wrap_content" android:layout_weight="1"/> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml old mode 100644 new mode 100755 index 371151f2ec8..449be093ac5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml @@ -19,3 +19,4 @@ android:id="@+id/expanded_menu" android:layout_width="?attr/panelMenuListWidth" android:layout_height="wrap_content" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml old mode 100644 new mode 100755 index d9c3f068114..f1b0a0708e5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml @@ -24,3 +24,4 @@ android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml old mode 100644 new mode 100755 index acd005a13b9..e4613d5b469 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml @@ -26,3 +26,4 @@ android:scaleType="centerInside" android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml old mode 100644 new mode 100755 index 1cee43e7048..e31c0b9e2e5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml @@ -58,3 +58,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml old mode 100644 new mode 100755 index 0ca8d7a2a5d..7bc5206980e --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml @@ -22,3 +22,4 @@ android:focusable="false" android:clickable="false" android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml old mode 100644 new mode 100755 index 76820e0789a..0d10365961d --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml @@ -59,3 +59,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml old mode 100644 new mode 100755 index 6e21f2e339f..7a6d272579e --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml @@ -24,3 +24,4 @@ android:foreground="?android:attr/windowContentOverlay" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml old mode 100644 new mode 100755 index 426851c20aa..f96f64ffd8b --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml @@ -32,3 +32,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml old mode 100644 new mode 100755 index ac399c1d7c7..468fe145b68 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml @@ -36,4 +36,4 @@ enabled. android:layout_width="match_parent" android:layout_height="wrap_content" /> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml old mode 100644 new mode 100755 index 572ec3463b3..2ab548b6a2d --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml @@ -51,3 +51,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml old mode 100644 new mode 100755 index 7407498a55e..dfc0d7ebb07 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml @@ -83,3 +83,4 @@ android:layout_above="@android:id/text2" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml old mode 100644 new mode 100755 index ff9361dcaf7..00cfd331bd0 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml @@ -139,3 +139,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml old mode 100644 new mode 100755 index 12bcbf1f3c2..a4011d84999 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml @@ -32,4 +32,4 @@ android:paddingTop="@dimen/abc_dialog_list_padding_vertical_material" android:paddingBottom="@dimen/abc_dialog_list_padding_vertical_material" android:clipToPadding="false" - style="@style/Widget.AppCompat.ListView" /> \ No newline at end of file + style="@style/Widget.AppCompat.ListView" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml old mode 100644 new mode 100755 index 8326b5ccfa1..c76ab753dd2 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml @@ -20,4 +20,4 @@ android:singleLine="true" android:layout_margin="3dip" android:layout_width="match_parent" - android:layout_height="wrap_content" /> \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml old mode 100644 new mode 100755 similarity index 53% rename from mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml rename to mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml index f0b6892a9f5..fa4b17192c6 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml @@ -14,8 +14,12 @@ limitations under the License. --> - - - 32dp - - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml new file mode 100755 index 00000000000..2ea0e6c7f81 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml @@ -0,0 +1,28 @@ + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml new file mode 100755 index 00000000000..2d953210850 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml new file mode 100755 index 00000000000..e1e4ff53af2 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml new file mode 100755 index 00000000000..7cdaaf96c9e --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml new file mode 100755 index 00000000000..0c53d24e307 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml new file mode 100755 index 00000000000..b76205cd605 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml new file mode 100755 index 00000000000..e5d3864ebd9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml old mode 100644 new mode 100755 index 677b178d6db..91f62f8a777 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml @@ -31,3 +31,4 @@ android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingRight="?attr/listPreferredItemPaddingRight" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml old mode 100644 new mode 100755 index 8f75bd34bd8..76d2c37bdb1 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml @@ -27,3 +27,4 @@ android:paddingRight="?attr/dialogPreferredPadding" android:checkMark="?android:attr/listChoiceIndicatorMultiple" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml old mode 100644 new mode 100755 index 3c2264cb8fb..eeccbc2d7a6 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml @@ -27,3 +27,4 @@ android:paddingRight="?attr/dialogPreferredPadding" android:checkMark="?android:attr/listChoiceIndicatorSingle" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml old mode 100644 new mode 100755 index d2f177ac8cb..7c177889cff --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml @@ -22,4 +22,4 @@ android:singleLine="true" android:layout_width="match_parent" android:layout_height="?attr/dropdownListPreferredItemHeight" - android:ellipsize="marquee"/> \ No newline at end of file + android:ellipsize="marquee"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml deleted file mode 100644 index f7348c46b65..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Klaar" - "Navigeer tuis" - "Navigeer op" - "Nog opsies" - "Soek" - "Soeknavraag" - "Vee navraag uit" - "Dien navraag in" - "Stemsoektog" - "Kies \'n program" - "Sien alles" - "Deel met %s" - "Deel met" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml new file mode 100755 index 00000000000..8050f0de0e9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml @@ -0,0 +1,18 @@ + + + + + "Navigeer tuis" + "Navigeer op" + "Nog opsies" + "Klaar" + "Sien alles" + "Kies \'n program" + "Vee navraag uit" + "Soeknavraag" + "Soek" + "Dien navraag in" + "Stemsoektog" + "Deel met" + "Deel met %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml deleted file mode 100644 index e849b31861f..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "ተከናውኗል" - "ወደ መነሻ ይዳስሱ" - "ወደ ላይ ይዳስሱ" - "ተጨማሪ አማራጮች" - "ፍለጋ" - "የፍለጋ ጥያቄ" - "መጠይቅ አጽዳ" - "መጠይቅ ያስረክቡ" - "የድምፅ ፍለጋ" - "መተግበሪያ ይምረጡ" - "ሁሉንም ይመልከቱ" - "ከ%s ጋር ያጋሩ" - "ከሚከተለው ጋር ያጋሩ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml new file mode 100755 index 00000000000..bceeaf6ce97 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml @@ -0,0 +1,18 @@ + + + + + "ወደ መነሻ ይዳስሱ" + "ወደ ላይ ይዳስሱ" + "ተጨማሪ አማራጮች" + "ተከናውኗል" + "ሁሉንም ይመልከቱ" + "መተግበሪያ ይምረጡ" + "መጠይቅ አጽዳ" + "የፍለጋ ጥያቄ" + "ፍለጋ" + "መጠይቅ ያስረክቡ" + "የድምፅ ፍለጋ" + "ከሚከተለው ጋር ያጋሩ" + "ከ%s ጋር ያጋሩ" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml deleted file mode 100644 index 93dde04224b..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "تم" - "التنقل إلى الشاشة الرئيسية" - "التنقل إلى أعلى" - "خيارات إضافية" - "بحث" - "طلب البحث" - "محو طلب البحث" - "إرسال طلب البحث" - "البحث الصوتي" - "اختيار تطبيق" - "عرض الكل" - "‏مشاركة مع %s" - "مشاركة مع" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml new file mode 100755 index 00000000000..495be021ae8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml @@ -0,0 +1,18 @@ + + + + + "التنقل إلى الشاشة الرئيسية" + "التنقل إلى أعلى" + "خيارات إضافية" + "تم" + "عرض الكل" + "اختيار تطبيق" + "محو طلب البحث" + "طلب البحث" + "بحث" + "إرسال طلب البحث" + "البحث الصوتي" + "مشاركة مع" + "‏مشاركة مع %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml deleted file mode 100644 index de3bde8f418..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Готово" - "Придвижване към „Начало“" - "Придвижване нагоре" - "Още опции" - "Търсене" - "Заявка за търсене" - "Изчистване на заявката" - "Изпращане на заявката" - "Гласово търсене" - "Изберете приложение" - "Вижте всички" - "Споделяне със: %s" - "Споделяне със:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml new file mode 100755 index 00000000000..39a0b8ce00d --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml @@ -0,0 +1,18 @@ + + + + + "Придвижване към „Начало“" + "Придвижване нагоре" + "Още опции" + "Готово" + "Вижте всички" + "Изберете приложение" + "Изчистване на заявката" + "Заявка за търсене" + "Търсене" + "Изпращане на заявката" + "Гласово търсене" + "Споделяне със:" + "Споделяне със: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml deleted file mode 100644 index 393240f9397..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "সম্পন্ন হয়েছে" - "হোম এ নেভিগেট করুন" - "উপরের দিকে নেভিগেট করুন" - "আরো বিকল্প" - "অনুসন্ধান করুন" - "ক্যোয়ারী অনুসন্ধান করুন" - "ক্যোয়ারী সাফ করুন" - "ক্যোয়ারী জমা দিন" - "ভয়েস অনুসন্ধান" - "একটি অ্যাপ্লিকেশান চয়ন করুন" - "সবগুলো দেখুন" - "%s এর সাথে ভাগ করুন" - "এর সাথে ভাগ করুন" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml new file mode 100755 index 00000000000..078dd47220f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml @@ -0,0 +1,18 @@ + + + + + "হোম এ নেভিগেট করুন" + "উপরের দিকে নেভিগেট করুন" + "আরো বিকল্প" + "সম্পন্ন হয়েছে" + "সবগুলো দেখুন" + "একটি অ্যাপ্লিকেশান চয়ন করুন" + "ক্যোয়ারী সাফ করুন" + "ক্যোয়ারী অনুসন্ধান করুন" + "অনুসন্ধান করুন" + "ক্যোয়ারী জমা দিন" + "ভয়েস অনুসন্ধান" + "এর সাথে ভাগ করুন" + "%s এর সাথে ভাগ করুন" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml deleted file mode 100644 index bfd4cb07a09..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fet" - "Navega a la pàgina d\'inici" - "Navega cap a dalt" - "Més opcions" - "Cerca" - "Consulta de cerca" - "Esborra la consulta" - "Envia la consulta" - "Cerca per veu" - "Selecciona una aplicació" - "Mostra\'ls tots" - "Comparteix amb %s" - "Comparteix amb" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml new file mode 100755 index 00000000000..16e784bd6eb --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml @@ -0,0 +1,18 @@ + + + + + "Navega a la pàgina d\'inici" + "Navega cap a dalt" + "Més opcions" + "Fet" + "Mostra\'ls tots" + "Selecciona una aplicació" + "Esborra la consulta" + "Consulta de cerca" + "Cerca" + "Envia la consulta" + "Cerca per veu" + "Comparteix amb" + "Comparteix amb %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml deleted file mode 100644 index 1465fdcd90d..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Hotovo" - "Přejít na plochu" - "Přejít nahoru" - "Více možností" - "Hledat" - "Vyhledávací dotaz" - "Smazat dotaz" - "Odeslat dotaz" - "Hlasové vyhledávání" - "Vybrat aplikaci" - "Zobrazit vše" - "Sdílet pomocí %s" - "Sdílet pomocí" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml new file mode 100755 index 00000000000..ef5477c726e --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml @@ -0,0 +1,18 @@ + + + + + "Přejít na plochu" + "Přejít nahoru" + "Více možností" + "Hotovo" + "Zobrazit vše" + "Vybrat aplikaci" + "Smazat dotaz" + "Vyhledávací dotaz" + "Hledat" + "Odeslat dotaz" + "Hlasové vyhledávání" + "Sdílet pomocí" + "Sdílet pomocí %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml deleted file mode 100644 index b178513e30e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Luk" - "Naviger hjem" - "Naviger op" - "Flere muligheder" - "Søg" - "Søgeforespørgsel" - "Ryd forespørgslen" - "Indsend forespørgslen" - "Stemmesøgning" - "Vælg en app" - "Se alle" - "Del med %s" - "Del med" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml new file mode 100755 index 00000000000..3d9628d5228 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml @@ -0,0 +1,18 @@ + + + + + "Naviger hjem" + "Naviger op" + "Flere muligheder" + "Luk" + "Se alle" + "Vælg en app" + "Ryd forespørgslen" + "Søgeforespørgsel" + "Søg" + "Indsend forespørgslen" + "Stemmesøgning" + "Del med" + "Del med %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml deleted file mode 100644 index 6da4b71340b..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fertig" - "Zur Startseite" - "Nach oben" - "Weitere Optionen" - "Suchen" - "Suchanfrage" - "Suchanfrage löschen" - "Suchanfrage senden" - "Sprachsuche" - "App auswählen" - "Alle ansehen" - "Freigeben für %s" - "Freigeben für" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml new file mode 100755 index 00000000000..33867f270ae --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml @@ -0,0 +1,18 @@ + + + + + "Zur Startseite" + "Nach oben" + "Weitere Optionen" + "Fertig" + "Alle ansehen" + "App auswählen" + "Suchanfrage löschen" + "Suchanfrage" + "Suchen" + "Suchanfrage senden" + "Sprachsuche" + "Freigeben für" + "Freigeben für %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml deleted file mode 100644 index 4c0e286f8af..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Τέλος" - "Πλοήγηση στην αρχική σελίδα" - "Πλοήγηση προς τα επάνω" - "Περισσότερες επιλογές" - "Αναζήτηση" - "Ερώτημα αναζήτησης" - "Διαγραφή ερωτήματος" - "Υποβολή ερωτήματος" - "Φωνητική αναζήτηση" - "Επιλέξτε κάποια εφαρμογή" - "Προβολή όλων" - "Κοινή χρήση με %s" - "Κοινή χρήση με" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml new file mode 100755 index 00000000000..24b8e5185b4 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml @@ -0,0 +1,18 @@ + + + + + "Πλοήγηση στην αρχική σελίδα" + "Πλοήγηση προς τα επάνω" + "Περισσότερες επιλογές" + "Τέλος" + "Προβολή όλων" + "Επιλέξτε κάποια εφαρμογή" + "Διαγραφή ερωτήματος" + "Ερώτημα αναζήτησης" + "Αναζήτηση" + "Υποβολή ερωτήματος" + "Φωνητική αναζήτηση" + "Κοινή χρήση με" + "Κοινή χρήση με %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml deleted file mode 100644 index 3ec0b0e9eed..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Finished" - "Navigate home" - "Navigate up" - "More options" - "Search" - "Search query" - "Clear query" - "Submit query" - "Voice search" - "Choose an app" - "See all" - "Share with %s" - "Share with" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml new file mode 100755 index 00000000000..cd0bcbe46a3 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml @@ -0,0 +1,18 @@ + + + + + "Navigate home" + "Navigate up" + "More options" + "Finished" + "See all" + "Choose an app" + "Clear query" + "Search query" + "Search" + "Submit query" + "Voice search" + "Share with" + "Share with %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml deleted file mode 100644 index 3ec0b0e9eed..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Finished" - "Navigate home" - "Navigate up" - "More options" - "Search" - "Search query" - "Clear query" - "Submit query" - "Voice search" - "Choose an app" - "See all" - "Share with %s" - "Share with" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml new file mode 100755 index 00000000000..ab5fea65965 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml @@ -0,0 +1,18 @@ + + + + + "Navigate home" + "Navigate up" + "More options" + "Finished" + "See all" + "Choose an app" + "Clear query" + "Search query" + "Search" + "Submit query" + "Voice search" + "Share with" + "Share with %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml deleted file mode 100644 index 6ab7942b95a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Listo" - "Navegar a la página principal" - "Navegar hacia arriba" - "Más opciones" - "Búsqueda" - "Consulta de búsqueda" - "Eliminar la consulta" - "Enviar consulta" - "Búsqueda por voz" - "Elige una aplicación." - "Ver todo" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml new file mode 100755 index 00000000000..df9949f0b5a --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml @@ -0,0 +1,18 @@ + + + + + "Navegar a la página principal" + "Navegar hacia arriba" + "Más opciones" + "Listo" + "Ver todo" + "Elige una aplicación." + "Eliminar la consulta" + "Consulta de búsqueda" + "Búsqueda" + "Enviar consulta" + "Búsqueda por voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml deleted file mode 100644 index ed15b35c447..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Listo" - "Ir a la pantalla de inicio" - "Desplazarse hacia arriba" - "Más opciones" - "Buscar" - "Consulta" - "Borrar consulta" - "Enviar consulta" - "Búsqueda por voz" - "Seleccionar una aplicación" - "Ver todo" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml new file mode 100755 index 00000000000..329ddece05a --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml @@ -0,0 +1,18 @@ + + + + + "Ir a la pantalla de inicio" + "Desplazarse hacia arriba" + "Más opciones" + "Listo" + "Ver todo" + "Seleccionar una aplicación" + "Borrar consulta" + "Consulta" + "Buscar" + "Enviar consulta" + "Búsqueda por voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml deleted file mode 100644 index 2ae925d407e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Valmis" - "Navigeerimine avaekraanile" - "Navigeerimine üles" - "Rohkem valikuid" - "Otsing" - "Otsingupäring" - "Päringu tühistamine" - "Päringu esitamine" - "Häälotsing" - "Valige rakendus" - "Kuva kõik" - "Jagamine kasutajaga %s" - "Jagamine:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml new file mode 100755 index 00000000000..2caae5866c7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml @@ -0,0 +1,18 @@ + + + + + "Navigeerimine avaekraanile" + "Navigeerimine üles" + "Rohkem valikuid" + "Valmis" + "Kuva kõik" + "Valige rakendus" + "Päringu tühistamine" + "Otsingupäring" + "Otsing" + "Päringu esitamine" + "Häälotsing" + "Jagamine:" + "Jagamine kasutajaga %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml deleted file mode 100644 index ee6ac4d2fad..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Eginda" - "Joan orri nagusira" - "Joan gora" - "Aukera gehiago" - "Bilatu" - "Bilaketa-kontsulta" - "Garbitu kontsulta" - "Bidali kontsulta" - "Ahots bidezko bilaketa" - "Aukeratu aplikazio bat" - "Ikusi guztiak" - "Partekatu %s erabiltzailearekin" - "Partekatu hauekin" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml new file mode 100755 index 00000000000..e09765764e6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml @@ -0,0 +1,18 @@ + + + + + "Joan orri nagusira" + "Joan gora" + "Aukera gehiago" + "Eginda" + "Ikusi guztiak" + "Aukeratu aplikazio bat" + "Garbitu kontsulta" + "Bilaketa-kontsulta" + "Bilatu" + "Bidali kontsulta" + "Ahots bidezko bilaketa" + "Partekatu hauekin" + "Partekatu %s erabiltzailearekin" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml deleted file mode 100644 index 8e10e92b1ba..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "انجام شد" - "پیمایش به صفحه اصلی" - "پیمایش به بالا" - "گزینه‌های بیشتر" - "جستجو" - "عبارت جستجو" - "پاک کردن عبارت جستجو" - "ارسال عبارت جستجو" - "جستجوی شفاهی" - "انتخاب برنامه" - "مشاهده همه" - "‏اشتراک‌گذاری با %s" - "اشتراک‌گذاری با" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml new file mode 100755 index 00000000000..1bd735485f0 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml @@ -0,0 +1,18 @@ + + + + + "پیمایش به صفحه اصلی" + "پیمایش به بالا" + "گزینه‌های بیشتر" + "انجام شد" + "مشاهده همه" + "انتخاب برنامه" + "پاک کردن عبارت جستجو" + "عبارت جستجو" + "جستجو" + "ارسال عبارت جستجو" + "جستجوی شفاهی" + "اشتراک‌گذاری با" + "‏اشتراک‌گذاری با %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml deleted file mode 100644 index 6755ceaac2e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Valmis" - "Siirry etusivulle" - "Siirry ylös" - "Lisää" - "Haku" - "Hakulauseke" - "Tyhjennä kysely" - "Lähetä kysely" - "Puhehaku" - "Valitse sovellus" - "Näytä kaikki" - "Jakaminen: %s" - "Jakaminen:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml new file mode 100755 index 00000000000..28ecbb9c335 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml @@ -0,0 +1,18 @@ + + + + + "Siirry etusivulle" + "Siirry ylös" + "Lisää" + "Valmis" + "Näytä kaikki" + "Valitse sovellus" + "Tyhjennä kysely" + "Hakulauseke" + "Haku" + "Lähetä kysely" + "Puhehaku" + "Jakaminen:" + "Jakaminen: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml deleted file mode 100644 index 417705a74fc..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Terminé" - "Revenir à l\'accueil" - "Revenir en haut de la page" - "Plus d\'options" - "Rechercher" - "Requête de recherche" - "Effacer la requête" - "Envoyer la requête" - "Recherche vocale" - "Sélectionnez une application" - "Voir toutes les chaînes" - "Partager avec %s" - "Partager avec" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml new file mode 100755 index 00000000000..703a48a9249 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml @@ -0,0 +1,18 @@ + + + + + "Revenir à l\'accueil" + "Revenir en haut de la page" + "Plus d\'options" + "Terminé" + "Voir toutes les chaînes" + "Sélectionnez une application" + "Effacer la requête" + "Requête de recherche" + "Rechercher" + "Envoyer la requête" + "Recherche vocale" + "Partager avec" + "Partager avec %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml deleted file mode 100644 index 27b8f389ac4..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "OK" - "Revenir à l\'accueil" - "Revenir en haut de la page" - "Plus d\'options" - "Rechercher" - "Requête de recherche" - "Effacer la requête" - "Envoyer la requête" - "Recherche vocale" - "Sélectionner une application" - "Tout afficher" - "Partager avec %s" - "Partager avec" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml new file mode 100755 index 00000000000..ee73b76ceb7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml @@ -0,0 +1,18 @@ + + + + + "Revenir à l\'accueil" + "Revenir en haut de la page" + "Plus d\'options" + "OK" + "Tout afficher" + "Sélectionner une application" + "Effacer la requête" + "Requête de recherche" + "Rechercher" + "Envoyer la requête" + "Recherche vocale" + "Partager avec" + "Partager avec %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml deleted file mode 100644 index 1d0d50de37e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Feito" - "Ir á páxina de inicio" - "Desprazarse cara arriba" - "Máis opcións" - "Buscar" - "Consulta de busca" - "Borrar consulta" - "Enviar consulta" - "Busca de voz" - "Escoller unha aplicación" - "Ver todas" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml new file mode 100755 index 00000000000..bd0a3ff757f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml @@ -0,0 +1,18 @@ + + + + + "Ir á páxina de inicio" + "Desprazarse cara arriba" + "Máis opcións" + "Feito" + "Ver todas" + "Escoller unha aplicación" + "Borrar consulta" + "Consulta de busca" + "Buscar" + "Enviar consulta" + "Busca de voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml deleted file mode 100644 index 09c43f0ef4a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - 54dip - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml new file mode 100755 index 00000000000..7d80f6bc772 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml @@ -0,0 +1,6 @@ + + + + + 54dip + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml deleted file mode 100644 index 442ea292a00..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml new file mode 100755 index 00000000000..d9db7afd29c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml deleted file mode 100644 index b236ebb5216..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "पूर्ण" - "मुखपृष्ठ पर नेविगेट करें" - "ऊपर नेविगेट करें" - "अधिक विकल्प" - "खोजें" - "खोज क्वेरी" - "क्‍वेरी साफ़ करें" - "क्वेरी सबमिट करें" - "ध्वनि खोज" - "कोई एप्‍लिकेशन चुनें" - "सभी देखें" - "%s के साथ साझा करें" - "इसके द्वारा साझा करें" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml new file mode 100755 index 00000000000..5cee91f94a0 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml @@ -0,0 +1,18 @@ + + + + + "मुखपृष्ठ पर नेविगेट करें" + "ऊपर नेविगेट करें" + "अधिक विकल्प" + "पूर्ण" + "सभी देखें" + "कोई एप्‍लिकेशन चुनें" + "क्‍वेरी साफ़ करें" + "खोज क्वेरी" + "खोजें" + "क्वेरी सबमिट करें" + "ध्वनि खोज" + "इसके द्वारा साझा करें" + "%s के साथ साझा करें" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml deleted file mode 100644 index 680e39f9753..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Gotovo" - "Idi na početnu" - "Idi gore" - "Dodatne opcije" - "Pretraživanje" - "Upit za pretraživanje" - "Izbriši upit" - "Pošalji upit" - "Glasovno pretraživanje" - "Odabir aplikacije" - "Prikaži sve" - "Dijeljenje sa: %s" - "Dijeljenje sa" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml new file mode 100755 index 00000000000..cfbad3e8b33 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml @@ -0,0 +1,18 @@ + + + + + "Idi na početnu" + "Idi gore" + "Dodatne opcije" + "Gotovo" + "Prikaži sve" + "Odabir aplikacije" + "Izbriši upit" + "Upit za pretraživanje" + "Pretraživanje" + "Pošalji upit" + "Glasovno pretraživanje" + "Dijeljenje sa" + "Dijeljenje sa: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml deleted file mode 100644 index 52dafb0e9f3..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Kész" - "Ugrás a főoldalra" - "Felfelé mozgatás" - "További lehetőségek" - "Keresés" - "Keresési lekérdezés" - "Lekérdezés törlése" - "Lekérdezés küldése" - "Hangalapú keresés" - "Válasszon ki egy alkalmazást" - "Összes megtekintése" - "Megosztás a következővel: %s" - "Megosztás a következővel:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml new file mode 100755 index 00000000000..41362066e54 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml @@ -0,0 +1,18 @@ + + + + + "Ugrás a főoldalra" + "Felfelé mozgatás" + "További lehetőségek" + "Kész" + "Összes megtekintése" + "Válasszon ki egy alkalmazást" + "Lekérdezés törlése" + "Keresési lekérdezés" + "Keresés" + "Lekérdezés küldése" + "Hangalapú keresés" + "Megosztás a következővel:" + "Megosztás a következővel: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml deleted file mode 100644 index 6c0ee27cc5e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Կատարված է" - "Ուղղվել տուն" - "Ուղղվել վերև" - "Այլ ընտրանքներ" - "Որոնել" - "Որոնման հարցում" - "Մաքրել հարցումը" - "Ուղարկել հարցումը" - "Ձայնային որոնում" - "Ընտրել ծրագիր" - "Տեսնել բոլորը" - "Տարածել ըստ %s" - "Տարածել" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml new file mode 100755 index 00000000000..d7b69d12c9b --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml @@ -0,0 +1,18 @@ + + + + + "Ուղղվել տուն" + "Ուղղվել վերև" + "Այլ ընտրանքներ" + "Կատարված է" + "Տեսնել բոլորը" + "Ընտրել ծրագիր" + "Մաքրել հարցումը" + "Որոնման հարցում" + "Որոնել" + "Ուղարկել հարցումը" + "Ձայնային որոնում" + "Տարածել" + "Տարածել ըստ %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml deleted file mode 100644 index 9481e83635c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Selesai" - "Navigasi ke beranda" - "Navigasi naik" - "Opsi lain" - "Telusuri" - "Kueri penelusuran" - "Hapus kueri" - "Kirim kueri" - "Penelusuran suara" - "Pilih aplikasi" - "Lihat semua" - "Bagikan dengan %s" - "Bagikan dengan" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml new file mode 100755 index 00000000000..a78f16e6956 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml @@ -0,0 +1,18 @@ + + + + + "Navigasi ke beranda" + "Navigasi naik" + "Opsi lain" + "Selesai" + "Lihat semua" + "Pilih aplikasi" + "Hapus kueri" + "Kueri penelusuran" + "Telusuri" + "Kirim kueri" + "Penelusuran suara" + "Bagikan dengan" + "Bagikan dengan %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml deleted file mode 100644 index 8cac57028b3..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Lokið" - "Fara heim" - "Fara upp" - "Fleiri valkostir" - "Leita" - "Leitarfyrirspurn" - "Hreinsa fyrirspurn" - "Senda fyrirspurn" - "Raddleit" - "Veldu forrit" - "Sjá allt" - "Deila með %s" - "Deila með" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml new file mode 100755 index 00000000000..10416a84ca9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml @@ -0,0 +1,18 @@ + + + + + "Fara heim" + "Fara upp" + "Fleiri valkostir" + "Lokið" + "Sjá allt" + "Veldu forrit" + "Hreinsa fyrirspurn" + "Leitarfyrirspurn" + "Leita" + "Senda fyrirspurn" + "Raddleit" + "Deila með" + "Deila með %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml deleted file mode 100644 index a8b0f2c1625..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fine" - "Vai alla home page" - "Vai in alto" - "Altre opzioni" - "Cerca" - "Query di ricerca" - "Cancella query" - "Invia query" - "Ricerca vocale" - "Scegli un\'applicazione" - "Visualizza tutte" - "Condividi con %s" - "Condividi con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml new file mode 100755 index 00000000000..e12fdb0e6b6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml @@ -0,0 +1,18 @@ + + + + + "Vai alla home page" + "Vai in alto" + "Altre opzioni" + "Fine" + "Visualizza tutte" + "Scegli un\'applicazione" + "Cancella query" + "Query di ricerca" + "Cerca" + "Invia query" + "Ricerca vocale" + "Condividi con" + "Condividi con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml deleted file mode 100644 index 1af07df88e6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "בוצע" - "נווט לדף הבית" - "נווט למעלה" - "עוד אפשרויות" - "חפש" - "שאילתת חיפוש" - "מחק שאילתה" - "שלח שאילתה" - "חיפוש קולי" - "בחר אפליקציה" - "ראה הכול" - "‏שתף עם %s" - "שתף עם" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml new file mode 100755 index 00000000000..db237295ae8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml @@ -0,0 +1,18 @@ + + + + + "נווט לדף הבית" + "נווט למעלה" + "עוד אפשרויות" + "בוצע" + "ראה הכול" + "בחר אפליקציה" + "מחק שאילתה" + "שאילתת חיפוש" + "חפש" + "שלח שאילתה" + "חיפוש קולי" + "שתף עם" + "‏שתף עם %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml deleted file mode 100644 index 659358a74c6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完了" - "ホームへ移動" - "上へ移動" - "その他のオプション" - "検索" - "検索キーワード" - "検索キーワードを削除" - "検索キーワードを送信" - "音声検索" - "アプリの選択" - "すべて表示" - "%sと共有" - "共有" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml new file mode 100755 index 00000000000..167cc1245e1 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml @@ -0,0 +1,18 @@ + + + + + "ホームへ移動" + "上へ移動" + "その他のオプション" + "完了" + "すべて表示" + "アプリの選択" + "検索キーワードを削除" + "検索キーワード" + "検索" + "検索キーワードを送信" + "音声検索" + "共有" + "%sと共有" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml deleted file mode 100644 index 0c430b1c440..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "დასრულდა" - "მთავარზე ნავიგაცია" - "ზემოთ ნავიგაცია" - "მეტი ვარიანტები" - "ძიება" - "ძიების მოთხოვნა" - "მოთხოვნის გასუფთავება" - "მოთხოვნის გადაგზავნა" - "ხმოვანი ძიება" - "აპის არჩევა" - "ყველას ნახვა" - "%s-თან გაზიარება" - "გაზიარება:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml new file mode 100755 index 00000000000..5b2be7f1700 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml @@ -0,0 +1,18 @@ + + + + + "მთავარზე ნავიგაცია" + "ზემოთ ნავიგაცია" + "მეტი ვარიანტები" + "დასრულდა" + "ყველას ნახვა" + "აპის არჩევა" + "მოთხოვნის გასუფთავება" + "ძიების მოთხოვნა" + "ძიება" + "მოთხოვნის გადაგზავნა" + "ხმოვანი ძიება" + "გაზიარება:" + "%s-თან გაზიარება" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml deleted file mode 100644 index d3ad9e8753c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Орындалды" - "Негізгі бетте қозғалу" - "Жоғары қозғалу" - "Басқа опциялар" - "Іздеу" - "Сұрақты іздеу" - "Сұрақты жою" - "Сұрақты жіберу" - "Дауыс арқылы іздеу" - "Қолданбаны таңдау" - "Барлығын көру" - "%s бөлісу" - "Бөлісу" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml new file mode 100755 index 00000000000..ca71cffaef7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml @@ -0,0 +1,18 @@ + + + + + "Негізгі бетте қозғалу" + "Жоғары қозғалу" + "Басқа опциялар" + "Орындалды" + "Барлығын көру" + "Қолданбаны таңдау" + "Сұрақты жою" + "Сұрақты іздеу" + "Іздеу" + "Сұрақты жіберу" + "Дауыс арқылы іздеу" + "Бөлісу" + "%s бөлісу" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml deleted file mode 100644 index df783725896..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "រួចរាល់" - "រកមើល​ទៅ​ដើម" - "រកមើល​ឡើងលើ" - "ជម្រើស​ច្រើន​ទៀត" - "ស្វែងរក" - "ស្វែងរក​សំណួរ" - "សម្អាត​សំណួរ" - "ដាក់​​​ស្នើ​សំណួរ" - "ការស្វែងរក​សំឡេង" - "ជ្រើស​កម្មវិធី​​" - "មើល​ទាំងអស់" - "ចែករំលែក​ជាមួយ %s" - "ចែករំលែក​ជាមួយ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml new file mode 100755 index 00000000000..88d4282802f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml @@ -0,0 +1,18 @@ + + + + + "រកមើល​ទៅ​ដើម" + "រកមើល​ឡើងលើ" + "ជម្រើស​ច្រើន​ទៀត" + "រួចរាល់" + "មើល​ទាំងអស់" + "ជ្រើស​កម្មវិធី​​" + "សម្អាត​សំណួរ" + "ស្វែងរក​សំណួរ" + "ស្វែងរក" + "ដាក់​​​ស្នើ​សំណួរ" + "ការស្វែងរក​សំឡេង" + "ចែករំលែក​ជាមួយ" + "ចែករំលែក​ជាមួយ %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml deleted file mode 100644 index 6af7a394cfe..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "ಮುಗಿದಿದೆ" - "ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" - "ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" - "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" - "ಹುಡುಕು" - "ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ" - "ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು" - "ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು" - "ಧ್ವನಿ ಹುಡುಕಾಟ" - "ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ" - "ಎಲ್ಲವನ್ನೂ ನೋಡಿ" - "%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ" - "ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml new file mode 100755 index 00000000000..cbb7321dea6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml @@ -0,0 +1,18 @@ + + + + + "ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" + "ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" + "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" + "ಮುಗಿದಿದೆ" + "ಎಲ್ಲವನ್ನೂ ನೋಡಿ" + "ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ" + "ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು" + "ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ" + "ಹುಡುಕು" + "ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು" + "ಧ್ವನಿ ಹುಡುಕಾಟ" + "ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ" + "%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml deleted file mode 100644 index d3319758474..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "완료" - "홈 탐색" - "위로 탐색" - "옵션 더보기" - "검색" - "검색어" - "검색어 삭제" - "검색어 보내기" - "음성 검색" - "앱 선택" - "전체 보기" - "%s와(과) 공유" - "공유 대상" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml new file mode 100755 index 00000000000..01665e92043 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml @@ -0,0 +1,18 @@ + + + + + "홈 탐색" + "위로 탐색" + "옵션 더보기" + "완료" + "전체 보기" + "앱 선택" + "검색어 삭제" + "검색어" + "검색" + "검색어 보내기" + "음성 검색" + "공유 대상" + "%s와(과) 공유" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml deleted file mode 100644 index 52abd9f0d80..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Даяр" - "Үйгө багыттоо" - "Жогору" - "Көбүрөөк мүмкүнчүлүктөр" - "Издөө" - "Издөө талаптары" - "Талаптарды тазалоо" - "Талап жөнөтүү" - "Үн аркылуу издөө" - "Колдонмо тандоо" - "Бардыгын көрүү" - "%s аркылуу бөлүшүү" - "Бөлүшүү" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml new file mode 100755 index 00000000000..c3e1291b2f8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml @@ -0,0 +1,18 @@ + + + + + "Үйгө багыттоо" + "Жогору" + "Көбүрөөк мүмкүнчүлүктөр" + "Даяр" + "Бардыгын көрүү" + "Колдонмо тандоо" + "Талаптарды тазалоо" + "Издөө талаптары" + "Издөө" + "Талап жөнөтүү" + "Үн аркылуу издөө" + "Бөлүшүү" + "%s аркылуу бөлүшүү" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml deleted file mode 100644 index 7d1a1af3f3a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml deleted file mode 100644 index d0d990dc0b7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml deleted file mode 100644 index 08c4b0d7292..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - 48dp - - 0dp - - 14dp - - 12dp - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml new file mode 100755 index 00000000000..75a944b1607 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml @@ -0,0 +1,20 @@ + + + + + true + + + true + + + 48dp + 0dp + + + 32dp + + + 12dp + 14dp + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml deleted file mode 100644 index 7d1a1af3f3a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml deleted file mode 100644 index c4f04a300ea..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - true - - - 440dp - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml deleted file mode 100644 index de1cefcccee..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - 192dip - - 4 - - - 60% - - 90% - - 60% - - 90% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml deleted file mode 100644 index aafef5fbd53..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml deleted file mode 100644 index 4cf49666c3c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml old mode 100644 new mode 100755 similarity index 67% rename from mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml rename to mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml index 2ca5b6fb901..734fb2f0f69 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml @@ -1,31 +1,72 @@ - - - - + - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml new file mode 100755 index 00000000000..6e5ba69bde9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml @@ -0,0 +1,13 @@ + + + + + + + - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml deleted file mode 100644 index 5fdc73c72b7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml new file mode 100755 index 00000000000..8433f447449 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + - - - - - - - - - - - - - + - - - + + + + + + - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml new file mode 100755 index 00000000000..79ff579bebf --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml @@ -0,0 +1,6 @@ + + + + + 0px + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml deleted file mode 100644 index 1bf90ef2533..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml new file mode 100755 index 00000000000..438d393b4a6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml deleted file mode 100644 index 0840f7391fd..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Xong" - "Điều hướng về trang chủ" - "Điều hướng lên trên" - "Thêm tùy chọn" - "Tìm kiếm" - "Tìm kiếm truy vấn" - "Xóa truy vấn" - "Gửi truy vấn" - "Tìm kiếm bằng giọng nói" - "Chọn một ứng dụng" - "Xem tất cả" - "Chia sẻ với %s" - "Chia sẻ với" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml new file mode 100755 index 00000000000..8fd629bb077 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml @@ -0,0 +1,18 @@ + + + + + "Điều hướng về trang chủ" + "Điều hướng lên trên" + "Thêm tùy chọn" + "Xong" + "Xem tất cả" + "Chọn một ứng dụng" + "Xóa truy vấn" + "Tìm kiếm truy vấn" + "Tìm kiếm" + "Gửi truy vấn" + "Tìm kiếm bằng giọng nói" + "Chia sẻ với" + "Chia sẻ với %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml deleted file mode 100644 index e5b2456c8e6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - 3 - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml new file mode 100755 index 00000000000..2c8ee7dfc7c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml @@ -0,0 +1,6 @@ + + + + + 3 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml deleted file mode 100644 index 470f89be359..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml deleted file mode 100644 index e95b6ff8c34..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml new file mode 100755 index 00000000000..060127bcbef --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml @@ -0,0 +1,9 @@ + + + + + true + + + true + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml deleted file mode 100644 index dd6458be71f..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - 4 - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml new file mode 100755 index 00000000000..67b3b2ffe1b --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml @@ -0,0 +1,6 @@ + + + + + 4 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml deleted file mode 100644 index 252ba6a7b74..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - 5 - - - 192dip - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml new file mode 100755 index 00000000000..aef006da141 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml @@ -0,0 +1,7 @@ + + + + + 192dip + 5 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml deleted file mode 100644 index 05c5aabffff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - false - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml new file mode 100755 index 00000000000..66422e8a5d4 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml @@ -0,0 +1,6 @@ + + + + + false + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml deleted file mode 100644 index dea6c74dadd..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - 256dip - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml new file mode 100755 index 00000000000..50415154f21 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml @@ -0,0 +1,6 @@ + + + + + 256dip + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml deleted file mode 100644 index 05c5aabffff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - false - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml deleted file mode 100644 index 3eb29620ba9..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - 5 - - - 192dip - - - 50% - - 70% - - 60% - - 90% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml new file mode 100755 index 00000000000..35ed941e1d9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml @@ -0,0 +1,14 @@ + + + + + false + + + 192dip + 60% + 90% + 50% + 70% + 5 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml deleted file mode 100644 index 87b36b0be99..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "转到主屏幕" - "转到上一层级" - "更多选项" - "搜索" - "搜索查询" - "清除查询" - "提交查询" - "语音搜索" - "选择应用" - "查看全部" - "通过%s分享" - "分享方式" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml new file mode 100755 index 00000000000..495f124d06c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml @@ -0,0 +1,18 @@ + + + + + "转到主屏幕" + "转到上一层级" + "更多选项" + "完成" + "查看全部" + "选择应用" + "清除查询" + "搜索查询" + "搜索" + "提交查询" + "语音搜索" + "分享方式" + "通过%s分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml deleted file mode 100644 index f6a367d3a05..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "瀏覽主頁" - "向上瀏覽" - "更多選項" - "搜尋" - "搜尋查詢" - "清除查詢" - "提交查詢" - "語音搜尋" - "選擇應用程式" - "顯示全部" - "與「%s」分享" - "分享對象" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml new file mode 100755 index 00000000000..23ab4bed681 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml @@ -0,0 +1,18 @@ + + + + + "瀏覽主頁" + "向上瀏覽" + "更多選項" + "完成" + "顯示全部" + "選擇應用程式" + "清除查詢" + "搜尋查詢" + "搜尋" + "提交查詢" + "語音搜尋" + "分享對象" + "與「%s」分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml deleted file mode 100644 index c804ccf3a97..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "瀏覽首頁" - "向上瀏覽" - "更多選項" - "搜尋" - "搜尋查詢" - "清除查詢" - "提交查詢" - "語音搜尋" - "選擇應用程式" - "查看全部" - "與「%s」分享" - "選擇分享對象" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml new file mode 100755 index 00000000000..8e253eb6d9c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml @@ -0,0 +1,18 @@ + + + + + "瀏覽首頁" + "向上瀏覽" + "更多選項" + "完成" + "查看全部" + "選擇應用程式" + "清除查詢" + "搜尋查詢" + "搜尋" + "提交查詢" + "語音搜尋" + "選擇分享對象" + "與「%s」分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml deleted file mode 100644 index 92eac7ecaf9..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Kwenziwe" - "Zulazulela ekhaya" - "Zulazulela phezulu" - "Izinketho eziningi" - "Sesha" - "Umbuzo wosesho" - "Sula inkinga" - "Hambisa umbuzo" - "Ukusesha ngezwi" - "Khetha uhlelo lokusebenza" - "Buka konke" - "Yabelana no-%s" - "Yabelana no-" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml new file mode 100755 index 00000000000..3c8273b5c5c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml @@ -0,0 +1,18 @@ + + + + + "Zulazulela ekhaya" + "Zulazulela phezulu" + "Izinketho eziningi" + "Kwenziwe" + "Buka konke" + "Khetha uhlelo lokusebenza" + "Sula inkinga" + "Umbuzo wosesho" + "Sesha" + "Hambisa umbuzo" + "Ukusesha ngezwi" + "Yabelana no-" + "Yabelana no-%s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml deleted file mode 100644 index 693da4c4ced..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml +++ /dev/null @@ -1,927 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml deleted file mode 100644 index 79a5035ad9a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - true - false - true - - false - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml deleted file mode 100644 index 32142019f09..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - #7fa87f - @android:color/black - @android:color/black - - @android:color/black - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml deleted file mode 100644 index 6b3cca5d484..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - #ff303030 - #ffeeeeee - #ff424242 - #ffeeeeee - - #ff212121 - #ffefefef - #ff000000 - #ff757575 - - #4dffffff - #1f000000 - - @color/material_deep_teal_500 - @color/material_deep_teal_200 - - #ff5a595b - #ffd6d7d7 - - #ffbdbdbd - #fff1f1f1 - #ff616161 - #ffbdbdbd - - @android:color/white - @android:color/black - - #80ffffff - - #80000000 - @color/bright_foreground_material_light - @color/bright_foreground_material_dark - - #ffbebebe - #ff323232 - #80bebebe - #80323232 - - @color/bright_foreground_disabled_material_dark - @color/bright_foreground_disabled_material_light - - - #6680cbc4 - - #66009688 - - @color/material_deep_teal_200 - @color/material_deep_teal_500 - - - - - #de000000 - #8a000000 - - #ffffffff - #b3ffffff - - 0.26 - 0.30 - - - #39000000 - #24000000 - - - #4Dffffff - #36ffffff - - - - - #ff80cbc4 - #ff009688 - - #ff37474f - #ff263238 - #ff21272b - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml deleted file mode 100644 index be6a7a1a120..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - false - - - 320dp - - - true - - - 150 - 220 - - true - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml deleted file mode 100644 index b8fa76c8e5c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - 2 - - - 180dp - - - 48dp - - 40dp - - 296dp - - - 160dip - - 320dip - - - 8dip - 8dip - - 32dip - - - - 320dp - - 320dp - - 80% - - 100% - - 6dp - @dimen/abc_control_inset_material - - @dimen/abc_control_padding_material - 8dp - - - 4dp - - 4dp - - 2dp - - 4dp - 10dp - 7dp - - - 3dp - - 24dp - 18dp - - - 48dp - - - 8dp - - - 65% - - - 95% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml deleted file mode 100644 index 2d0f738133e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - 56dp - - 4dp - - 16dp - - 16dp - - -3dp - - 5dp - - - @dimen/abc_action_bar_content_inset_material - - - 0dp - - 6dp - - 10dp - - 36dp - 48dp - 48dp - - 112sp - 56sp - 45sp - 34sp - 24sp - 20sp - 16sp - 20dp - 16dp - 16sp - 14sp - 14sp - 12sp - 14sp - - 22sp - 18sp - 14sp - - 16dp - - 0.26 - 0.30 - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml deleted file mode 100644 index 2e6ef2497eb..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml deleted file mode 100644 index 87cfd29cf96..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - Done - - Navigate home - - Navigate up - - More options - - - Collapse - - - %1$s, %2$s - - %1$s, %2$s, %3$s - - - Search - - Search… - - Search query - - Clear query - - Submit query - - Voice search - - - - Choose an app - - See all - - Share with %s - - Share with - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml deleted file mode 100644 index 1f30314c0ff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml deleted file mode 100644 index 53208008a52..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml deleted file mode 100644 index f8962df6b47..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml new file mode 100755 index 00000000000..54cf69519d2 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml @@ -0,0 +1,1517 @@ + + + + + + + + + + + true + false + true + + + true + false + true + + + false + + + @android:color/black + #7fa87f + @android:color/black + @android:color/black + + + @color/material_deep_teal_200 + @color/material_deep_teal_500 + #ff424242 + #ffeeeeee + #ff303030 + #ffeeeeee + #80ffffff + #80000000 + @color/bright_foreground_material_light + @color/bright_foreground_material_dark + @android:color/white + @android:color/black + #ff5a595b + #ffd6d7d7 + #80bebebe + #80323232 + #ffbebebe + #ff323232 + #6680cbc4 + #66009688 + @color/bright_foreground_disabled_material_dark + @color/bright_foreground_disabled_material_light + @color/material_deep_teal_200 + @color/material_deep_teal_500 + #ff37474f + #ff263238 + #ff21272b + #ff80cbc4 + #ff009688 + #ff000000 + #ff757575 + #ff212121 + #ffefefef + #ffffffff + #de000000 + #4Dffffff + #39000000 + #4dffffff + #1f000000 + #b3ffffff + #8a000000 + #36ffffff + #24000000 + #ff616161 + #ffbdbdbd + #ffbdbdbd + #fff1f1f1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16dp + 56dp + 4dp + 16dp + 0dp + 10dp + 6dp + + + 40dp + 48dp + 180dp + + + 5dp + -3dp + 48dp + 48dp + 36dp + + + 48dp + @dimen/abc_control_inset_material + 6dp + 8dp + @dimen/abc_control_padding_material + + + 320dp + + + 2dp + 4dp + 4dp + 8dp + 65% + 95% + 24dp + 18dp + + + 0.30 + 0.26 + + + 32dip + 8dip + 8dip + 7dp + 4dp + 10dp + + + 16dp + @dimen/abc_action_bar_content_inset_material + + + 296dp + 320dip + 160dip + 3dp + + + 14sp + 14sp + 14sp + 12sp + 34sp + 45sp + 56sp + 112sp + 24sp + 22sp + 18sp + 16sp + 14sp + 16sp + 16dp + 20sp + 20dp + + + 80% + 100% + 320dp + 320dp + + + 0.30 + 0.26 + + + 64dp + 64dp + 12dp + + + #3333B5E5 + + + + + + + + + + + + + + 220 + 150 + + + 2 + + + 127 + 999 + + + Navigate home + %1$s, %2$s + %1$s, %2$s, %3$s + Navigate up + More options + Done + See all + Choose an app + Search… + Clear query + Search query + Search + Submit query + Voice search + Share with + Share with %s + Collapse + 999+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +