From 21cd231707ca950f71d921536188a19acf6608c7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 19 Apr 2016 18:50:04 +0200 Subject: [PATCH 1/5] updated versions of Android-related libraries to better align Eclipse debug experience with production code Change-Id: I4b5367f3569a25e2aa446d399d72f32316594a31 --- .../com.sap.sailing.android.shared/.classpath | 33 +- .../project.properties | 2 + .../.classpath | 27 +- .../project.properties | 1 + mobile/github-advrecyclerview_lib/.classpath | 21 +- .../project.properties | 2 - .../animator/BaseItemAnimator.java | 151 - .../animator/GeneralItemAnimator.java | 285 -- .../RefactoredDefaultItemAnimator.java | 313 -- .../animator/SwipeDismissItemAnimator.java | 395 -- .../animator/impl/AddAnimationInfo.java | 46 - .../impl/BaseItemAnimationManager.java | 240 -- .../animator/impl/ChangeAnimationInfo.java | 67 - .../impl/ItemAddAnimationManager.java | 70 - .../animator/impl/ItemAnimationInfo.java | 26 - .../impl/ItemChangeAnimationManager.java | 90 - .../impl/ItemMoveAnimationManager.java | 70 - .../impl/ItemRemoveAnimationManager.java | 70 - .../animator/impl/MoveAnimationInfo.java | 55 - .../animator/impl/RemoveAnimationInfo.java | 46 - .../decoration/ItemShadowDecorator.java | 112 - .../SimpleListDividerDecorator.java | 115 - .../draggable/BaseDraggableItemDecorator.java | 108 - ...asicSwapTargetTranslationInterpolator.java | 50 - .../draggable/DraggableItemAdapter.java | 51 - .../draggable/DraggableItemViewHolder.java | 46 - .../DraggableItemWrapperAdapter.java | 359 -- .../draggable/DraggingItemDecorator.java | 282 -- .../draggable/EdgeEffectDecorator.java | 154 - .../draggable/ItemDraggableRange.java | 52 - .../RecyclerViewDragDropManager.java | 1110 ------ .../draggable/SwapTargetItemOperator.java | 197 - .../BaseRecyclerViewEventDistributor.java | 252 -- .../RecyclerViewEventDistributorListener.java | 22 - .../RecyclerViewOnScrollEventDistributor.java | 98 - .../RecyclerViewRecyclerEventDistributor.java | 81 - .../expandable/ExpandableAdapterHelper.java | 68 - .../ExpandableDraggableItemAdapter.java | 89 - .../expandable/ExpandableItemAdapter.java | 139 - .../expandable/ExpandableItemViewHolder.java | 46 - .../ExpandablePositionTranslator.java | 428 --- .../ExpandableRecyclerViewWrapperAdapter.java | 658 ---- .../ExpandableSwipeableItemAdapter.java | 159 - .../RecyclerViewExpandableItemManager.java | 472 --- .../swipeable/ItemSlidingAnimator.java | 247 -- .../swipeable/RecyclerViewSwipeManager.java | 750 ---- .../swipeable/RemovingItemDecorator.java | 244 -- .../swipeable/RubberBandInterpolator.java | 33 - .../swipeable/SwipeReactionUtils.java | 39 - .../swipeable/SwipeableItemAdapter.java | 84 - .../swipeable/SwipeableItemViewHolder.java | 113 - .../SwipeableItemWrapperAdapter.java | 272 -- .../swipeable/SwipingItemOperator.java | 100 - .../RecyclerViewTouchActionGuardManager.java | 188 - .../AbstractDraggableItemViewHolder.java | 40 - ...tractDraggableSwipeableItemViewHolder.java | 87 - .../utils/AbstractExpandableItemAdapter.java | 88 - .../AbstractExpandableItemViewHolder.java | 40 - .../AbstractSwipeableItemViewHolder.java | 75 - .../utils/BaseWrapperAdapter.java | 227 -- .../utils/CustomRecyclerViewUtils.java | 121 - .../utils/WrapperAdapterUtils.java | 53 - mobile/google-android-support-v13/.classpath | 10 + mobile/google-android-support-v13/.gitignore | 1 + mobile/google-android-support-v13/.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 11 + .../AndroidManifest.xml} | 12 +- .../libs/internal_impl-22.2.1.jar | Bin 0 -> 1708 bytes .../project.properties | 15 + .../src/META-INF/MANIFEST.MF | 2 + .../support/v13/app/FragmentCompat.java | 78 + .../support/v13/app/FragmentCompatICS.java | 25 + .../support/v13/app/FragmentCompatICSMR1.java | 25 + .../support/v13/app/FragmentPagerAdapter.java | 179 + .../v13/app/FragmentStatePagerAdapter.java | 229 ++ .../support/v13/app/FragmentTabHost.java | 347 ++ .../src/android/support/v13/app/package.html | 5 + mobile/google-android-support-v4/.classpath | 10 + mobile/google-android-support-v4/.gitignore | 1 + mobile/google-android-support-v4/.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../AndroidManifest.xml} | 11 +- .../v4/media/MediaMetadataCompat.aidl} | 15 +- .../support/v4/media/RatingCompat.aidl | 18 + .../v4/media/session/MediaSessionCompat.aidl | 20 + .../media/session/ParcelableVolumeInfo.aidl | 18 + .../v4/media/session/PlaybackStateCompat.aidl | 18 + .../libs/internal_impl-22.2.1.jar | Bin 0 -> 266310 bytes .../project.properties | 15 + .../src/META-INF/MANIFEST.MF | 2 + .../AccessibilityServiceInfoCompat.java | 451 +++ .../AccessibilityServiceInfoCompatIcs.java | 47 + ...sibilityServiceInfoCompatJellyBeanMr2.java | 30 + .../v4/accessibilityservice/package.html | 6 + .../v4/animation/AnimatorCompatHelper.java | 40 + .../v4/animation/AnimatorListenerCompat.java | 57 + .../v4/animation/AnimatorProvider.java | 35 + .../AnimatorUpdateListenerCompat.java | 36 + .../DonutAnimatorCompatProvider.java | 151 + .../HoneycombMr1AnimatorCompatProvider.java | 124 + .../v4/animation/ValueAnimatorCompat.java | 41 + .../support/v4/app/ActionBarDrawerToggle.java | 560 +++ .../app/ActionBarDrawerToggleHoneycomb.java | 140 + .../ActionBarDrawerToggleJellybeanMR2.java | 69 + .../support/v4/app/ActivityCompat.java | 262 ++ .../support/v4/app/ActivityCompat21.java | 124 + .../v4/app/ActivityCompatHoneycomb.java | 36 + .../support/v4/app/ActivityCompatJB.java | 36 + .../support/v4/app/ActivityManagerCompat.java | 44 + .../v4/app/ActivityManagerCompatKitKat.java | 25 + .../support/v4/app/ActivityOptionsCompat.java | 242 ++ .../v4/app/ActivityOptionsCompat21.java | 60 + .../v4/app/ActivityOptionsCompatJB.java | 58 + .../support/v4/app/BackStackRecord.java | 1457 +++++++ .../android/support/v4/app/BundleUtil.java | 27 + .../support/v4/app/DialogFragment.java | 452 +++ .../src/android/support/v4/app/Fragment.java | 2015 ++++++++++ .../support/v4/app/FragmentActivity.java | 880 +++++ .../support/v4/app/FragmentManager.java | 2224 +++++++++++ .../support/v4/app/FragmentPagerAdapter.java | 175 + .../v4/app/FragmentStatePagerAdapter.java | 225 ++ .../support/v4/app/FragmentTabHost.java | 355 ++ .../support/v4/app/FragmentTransaction.java | 306 ++ .../v4/app/FragmentTransitionCompat21.java | 383 ++ .../v4/app/INotificationSideChannel.aidl | 42 + .../android/support/v4/app/ListFragment.java | 375 ++ .../android/support/v4/app/LoaderManager.java | 833 ++++ .../src/android/support/v4/app/NavUtils.java | 309 ++ .../android/support/v4/app/NavUtilsJB.java | 39 + .../v4/app/NoSaveStateFrameLayout.java | 63 + .../app/NotificationBuilderWithActions.java | 24 + ...otificationBuilderWithBuilderAccessor.java | 31 + .../support/v4/app/NotificationCompat.java | 3360 +++++++++++++++++ .../v4/app/NotificationCompatApi20.java | 199 + .../v4/app/NotificationCompatApi21.java | 223 ++ .../v4/app/NotificationCompatBase.java | 57 + .../v4/app/NotificationCompatExtras.java | 70 + .../v4/app/NotificationCompatGingerbread.java | 31 + .../v4/app/NotificationCompatHoneycomb.java | 54 + .../NotificationCompatIceCreamSandwich.java | 69 + .../v4/app/NotificationCompatJellybean.java | 406 ++ .../v4/app/NotificationCompatKitKat.java | 156 + .../NotificationCompatSideChannelService.java | 118 + .../v4/app/NotificationManagerCompat.java | 628 +++ .../app/NotificationManagerCompatEclair.java | 32 + ...ficationManagerCompatIceCreamSandwich.java | 24 + .../android/support/v4/app/RemoteInput.java | 276 ++ .../v4/app/RemoteInputCompatApi20.java | 63 + .../support/v4/app/RemoteInputCompatBase.java | 36 + .../v4/app/RemoteInputCompatJellybean.java | 107 + .../android/support/v4/app/ServiceCompat.java | 45 + .../android/support/v4/app/ShareCompat.java | 957 +++++ .../support/v4/app/ShareCompatICS.java | 41 + .../android/support/v4/app/ShareCompatJB.java | 25 + .../support/v4/app/SharedElementCallback.java | 264 ++ .../v4/app/SuperNotCalledException.java | 25 + .../support/v4/app/TaskStackBuilder.java | 393 ++ .../v4/app/TaskStackBuilderHoneycomb.java | 31 + .../v4/app/TaskStackBuilderJellybean.java | 31 + .../src/android/support/v4/app/package.html | 8 + .../support/v4/content/AsyncTaskLoader.java | 286 ++ .../support/v4/content/ContextCompat.java | 389 ++ .../v4/content/ContextCompatApi21.java | 36 + .../v4/content/ContextCompatFroyo.java | 31 + .../v4/content/ContextCompatHoneycomb.java | 36 + .../v4/content/ContextCompatJellybean.java | 29 + .../v4/content/ContextCompatKitKat.java | 35 + .../support/v4/content/CursorLoader.java | 207 + .../support/v4/content/FileProvider.java | 773 ++++ .../support/v4/content/IntentCompat.java | 268 ++ .../v4/content/IntentCompatHoneycomb.java | 30 + .../v4/content/IntentCompatIcsMr1.java | 27 + .../android/support/v4/content/Loader.java | 405 ++ .../v4/content/LocalBroadcastManager.java | 302 ++ .../support/v4/content/ModernAsyncTask.java | 497 +++ .../v4/content/WakefulBroadcastReceiver.java | 134 + .../android/support/v4/content/package.html | 10 + .../v4/content/pm/ActivityInfoCompat.java | 35 + .../support/v4/content/pm/package.html | 6 + .../v4/content/res/ResourcesCompat.java | 93 + .../v4/content/res/ResourcesCompatApi21.java | 34 + .../v4/content/res/ResourcesCompatIcsMr1.java | 29 + .../v4/database/DatabaseUtilsCompat.java | 58 + .../android/support/v4/database/package.html | 6 + .../support/v4/graphics/BitmapCompat.java | 111 + .../v4/graphics/BitmapCompatHoneycombMr1.java | 29 + .../v4/graphics/BitmapCompatJellybeanMR2.java | 28 + .../v4/graphics/BitmapCompatKitKat.java | 29 + .../support/v4/graphics/ColorUtils.java | 297 ++ .../v4/graphics/drawable/DrawableCompat.java | 318 ++ .../drawable/DrawableCompatApi22.java | 31 + .../graphics/drawable/DrawableCompatBase.java | 53 + .../drawable/DrawableCompatHoneycomb.java | 38 + .../drawable/DrawableCompatKitKat.java | 41 + .../drawable/DrawableCompatLollipop.java | 80 + .../v4/graphics/drawable/DrawableWrapper.java | 41 + .../drawable/DrawableWrapperDonut.java | 245 ++ .../drawable/DrawableWrapperHoneycomb.java | 31 + .../drawable/DrawableWrapperKitKat.java | 36 + .../drawable/DrawableWrapperLollipop.java | 59 + .../drawable/RoundedBitmapDrawable.java | 327 ++ .../drawable/RoundedBitmapDrawable21.java | 56 + .../RoundedBitmapDrawableFactory.java | 98 + .../display/DisplayManagerCompat.java | 154 + .../display/DisplayManagerJellybeanMr1.java | 38 + .../support/v4/internal/view/SupportMenu.java | 56 + .../v4/internal/view/SupportMenuItem.java | 204 + .../v4/internal/view/SupportSubMenu.java | 29 + .../v4/media/MediaDescriptionCompat.java | 359 ++ .../v4/media/MediaDescriptionCompatApi21.java | 100 + .../support/v4/media/MediaMetadataCompat.aidl | 18 + .../support/v4/media/MediaMetadataCompat.java | 746 ++++ .../v4/media/MediaMetadataCompatApi21.java | 75 + .../support/v4/media/RatingCompat.aidl | 18 + .../support/v4/media/RatingCompat.java | 370 ++ .../support/v4/media/RatingCompatApi21.java | 65 + .../support/v4/media/TransportController.java | 92 + .../support/v4/media/TransportMediator.java | 343 ++ .../v4/media/TransportMediatorCallback.java | 26 + .../media/TransportMediatorJellybeanMR2.java | 207 + .../support/v4/media/TransportPerformer.java | 202 + .../v4/media/TransportStateListener.java | 39 + .../v4/media/VolumeProviderCompat.java | 171 + .../v4/media/VolumeProviderCompatApi21.java | 45 + .../media/routing/MediaRouterJellybean.java | 442 +++ .../routing/MediaRouterJellybeanMr1.java | 176 + .../routing/MediaRouterJellybeanMr2.java | 44 + .../session/IMediaControllerCallback.aidl | 40 + .../v4/media/session/IMediaSession.aidl | 69 + .../media/session/MediaControllerCompat.java | 1323 +++++++ .../session/MediaControllerCompatApi21.java | 281 ++ .../v4/media/session/MediaSessionCompat.aidl | 20 + .../v4/media/session/MediaSessionCompat.java | 1924 ++++++++++ .../session/MediaSessionCompatApi14.java | 250 ++ .../session/MediaSessionCompatApi18.java | 93 + .../session/MediaSessionCompatApi19.java | 101 + .../session/MediaSessionCompatApi21.java | 251 ++ .../session/MediaSessionCompatApi22.java | 25 + .../media/session/MediaSessionCompatApi8.java | 32 + .../media/session/ParcelableVolumeInfo.aidl | 18 + .../media/session/ParcelableVolumeInfo.java | 77 + .../v4/media/session/PlaybackStateCompat.aidl | 18 + .../v4/media/session/PlaybackStateCompat.java | 970 +++++ .../session/PlaybackStateCompatApi21.java | 103 + .../session/PlaybackStateCompatApi22.java | 46 + .../v4/net/ConnectivityManagerCompat.java | 121 + .../ConnectivityManagerCompatGingerbread.java | 57 + ...ConnectivityManagerCompatHoneycombMR2.java | 61 + .../ConnectivityManagerCompatJellyBean.java | 28 + .../support/v4/net/TrafficStatsCompat.java | 203 + .../support/v4/net/TrafficStatsCompatIcs.java | 55 + .../support/v4/os/AsyncTaskCompat.java | 54 + .../v4/os/AsyncTaskCompatHoneycomb.java | 32 + .../support/v4/os/EnvironmentCompat.java | 75 + .../v4/os/EnvironmentCompatKitKat.java | 27 + .../support/v4/os/ParcelableCompat.java | 59 + .../os/ParcelableCompatCreatorCallbacks.java | 47 + .../v4/os/ParcelableCompatHoneycombMR2.java | 46 + .../android/support/v4/os/TraceCompat.java | 60 + .../support/v4/os/TraceJellybeanMR2.java | 26 + .../src/android/support/v4/os/package.html | 6 + .../android/support/v4/print/PrintHelper.java | 353 ++ .../support/v4/print/PrintHelperKitkat.java | 544 +++ .../support/v4/provider/DocumentFile.java | 345 ++ .../v4/provider/DocumentsContractApi19.java | 197 + .../v4/provider/DocumentsContractApi21.java | 85 + .../support/v4/provider/RawDocumentFile.java | 174 + .../v4/provider/SingleDocumentFile.java | 107 + .../support/v4/provider/TreeDocumentFile.java | 119 + .../v4/speech/tts/TextToSpeechICS.java | 27 + .../v4/speech/tts/TextToSpeechICSMR1.java | 74 + .../support/v4/text/BidiFormatter.java | 880 +++++ .../android/support/v4/text/ICUCompat.java | 98 + .../android/support/v4/text/ICUCompatIcs.java | 79 + .../v4/text/TextDirectionHeuristicCompat.java | 46 + .../text/TextDirectionHeuristicsCompat.java | 257 ++ .../support/v4/text/TextUtilsCompat.java | 118 + .../src/android/support/v4/util/ArrayMap.java | 205 + .../android/support/v4/util/AtomicFile.java | 213 ++ .../support/v4/util/CircularArray.java | 252 ++ .../support/v4/util/CircularIntArray.java | 207 + .../support/v4/util/ContainerHelpers.java | 82 + .../android/support/v4/util/DebugUtils.java | 44 + .../android/support/v4/util/LogWriter.java | 69 + .../support/v4/util/LongSparseArray.java | 391 ++ .../src/android/support/v4/util/LruCache.java | 343 ++ .../support/v4/util/MapCollections.java | 558 +++ .../src/android/support/v4/util/Pair.java | 79 + .../src/android/support/v4/util/Pools.java | 166 + .../support/v4/util/SimpleArrayMap.java | 615 +++ .../support/v4/util/SparseArrayCompat.java | 386 ++ .../android/support/v4/util/TimeUtils.java | 175 + .../src/android/support/v4/util/package.html | 6 + .../v4/view/AccessibilityDelegateCompat.java | 502 +++ .../view/AccessibilityDelegateCompatIcs.java | 119 + .../AccessibilityDelegateCompatJellyBean.java | 106 + .../support/v4/view/ActionProvider.java | 261 ++ .../v4/view/GestureDetectorCompat.java | 564 +++ .../support/v4/view/GravityCompat.java | 203 + .../v4/view/GravityCompatJellybeanMr1.java | 42 + .../support/v4/view/InputDeviceCompat.java | 211 ++ .../support/v4/view/KeyEventCompat.java | 211 ++ .../support/v4/view/KeyEventCompatEclair.java | 40 + .../v4/view/KeyEventCompatHoneycomb.java | 36 + .../support/v4/view/LayoutInflaterCompat.java | 82 + .../v4/view/LayoutInflaterCompatBase.java | 48 + .../v4/view/LayoutInflaterCompatHC.java | 91 + .../v4/view/LayoutInflaterCompatLollipop.java | 27 + .../v4/view/LayoutInflaterFactory.java | 45 + .../v4/view/MarginLayoutParamsCompat.java | 233 ++ .../MarginLayoutParamsCompatJellybeanMr1.java | 55 + .../android/support/v4/view/MenuCompat.java | 36 + .../support/v4/view/MenuItemCompat.java | 440 +++ .../v4/view/MenuItemCompatHoneycomb.java | 41 + .../support/v4/view/MenuItemCompatIcs.java | 68 + .../support/v4/view/MotionEventCompat.java | 519 +++ .../v4/view/MotionEventCompatEclair.java | 40 + .../v4/view/MotionEventCompatGingerbread.java | 28 + .../view/MotionEventCompatHoneycombMr1.java | 32 + .../support/v4/view/NestedScrollingChild.java | 227 ++ .../v4/view/NestedScrollingChildHelper.java | 282 ++ .../v4/view/NestedScrollingParent.java | 196 + .../v4/view/NestedScrollingParentHelper.java | 84 + .../v4/view/OnApplyWindowInsetsListener.java | 43 + .../android/support/v4/view/PagerAdapter.java | 320 ++ .../support/v4/view/PagerTabStrip.java | 296 ++ .../support/v4/view/PagerTitleStrip.java | 512 +++ .../support/v4/view/PagerTitleStripIcs.java | 46 + .../v4/view/ScaleGestureDetectorCompat.java | 85 + .../ScaleGestureDetectorCompatKitKat.java | 39 + .../support/v4/view/ScrollingView.java | 134 + .../v4/view/TintableBackgroundView.java | 70 + .../v4/view/VelocityTrackerCompat.java | 93 + .../view/VelocityTrackerCompatHoneycomb.java | 31 + .../android/support/v4/view/ViewCompat.java | 3008 +++++++++++++++ .../support/v4/view/ViewCompatBase.java | 111 + .../support/v4/view/ViewCompatEclairMr1.java | 56 + .../v4/view/ViewCompatGingerbread.java | 29 + .../android/support/v4/view/ViewCompatHC.java | 163 + .../support/v4/view/ViewCompatICS.java | 57 + .../android/support/v4/view/ViewCompatJB.java | 88 + .../v4/view/ViewCompatJellybeanMr1.java | 66 + .../support/v4/view/ViewCompatKitKat.java | 40 + .../support/v4/view/ViewCompatLollipop.java | 162 + .../v4/view/ViewConfigurationCompat.java | 115 + .../v4/view/ViewConfigurationCompatFroyo.java | 28 + .../v4/view/ViewConfigurationCompatICS.java | 28 + .../support/v4/view/ViewGroupCompat.java | 267 ++ .../support/v4/view/ViewGroupCompatHC.java | 29 + .../support/v4/view/ViewGroupCompatIcs.java | 31 + .../v4/view/ViewGroupCompatJellybeanMR2.java | 30 + .../v4/view/ViewGroupCompatLollipop.java | 34 + .../android/support/v4/view/ViewPager.java | 2976 +++++++++++++++ .../support/v4/view/ViewParentCompat.java | 427 +++ .../support/v4/view/ViewParentCompatICS.java | 31 + .../v4/view/ViewParentCompatKitKat.java | 27 + .../v4/view/ViewParentCompatLollipop.java | 98 + .../v4/view/ViewPropertyAnimatorCompat.java | 1338 +++++++ .../view/ViewPropertyAnimatorCompatICS.java | 156 + .../v4/view/ViewPropertyAnimatorCompatJB.java | 59 + ...iewPropertyAnimatorCompatJellybeanMr2.java | 25 + .../v4/view/ViewPropertyAnimatorCompatKK.java | 33 + .../ViewPropertyAnimatorCompatLollipop.java | 39 + .../v4/view/ViewPropertyAnimatorListener.java | 47 + .../ViewPropertyAnimatorListenerAdapter.java | 49 + .../ViewPropertyAnimatorUpdateListener.java | 36 + .../android/support/v4/view/WindowCompat.java | 61 + .../support/v4/view/WindowInsetsCompat.java | 259 ++ .../v4/view/WindowInsetsCompatApi21.java | 118 + .../AccessibilityEventCompat.java | 316 ++ .../AccessibilityEventCompatIcs.java | 42 + .../AccessibilityEventCompatKitKat.java | 29 + .../AccessibilityManagerCompat.java | 216 ++ .../AccessibilityManagerCompatIcs.java | 69 + .../AccessibilityNodeInfoCompat.java | 2787 ++++++++++++++ .../AccessibilityNodeInfoCompatApi21.java | 83 + .../AccessibilityNodeInfoCompatApi22.java | 50 + .../AccessibilityNodeInfoCompatIcs.java | 219 ++ .../AccessibilityNodeInfoCompatJellyBean.java | 79 + ...cessibilityNodeInfoCompatJellybeanMr2.java | 30 + .../AccessibilityNodeInfoCompatKitKat.java | 127 + .../AccessibilityNodeProviderCompat.java | 248 ++ ...essibilityNodeProviderCompatJellyBean.java | 59 + ...AccessibilityNodeProviderCompatKitKat.java | 65 + .../AccessibilityRecordCompat.java | 1044 +++++ .../AccessibilityRecordCompatIcs.java | 193 + .../AccessibilityRecordCompatIcsMr1.java | 40 + .../AccessibilityRecordCompatJellyBean.java | 30 + .../v4/view/accessibility/package.html | 5 + .../FastOutLinearInInterpolator.java | 69 + .../animation/FastOutSlowInInterpolator.java | 70 + .../LinearOutSlowInInterpolator.java | 70 + .../animation/LookupTableInterpolator.java | 57 + .../animation/PathInterpolatorCompat.java | 85 + .../PathInterpolatorCompatApi21.java | 44 + .../animation/PathInterpolatorCompatBase.java | 43 + .../view/animation/PathInterpolatorDonut.java | 112 + .../src/android/support/v4/view/package.html | 11 + .../support/v4/widget/AutoScrollHelper.java | 867 +++++ .../support/v4/widget/CircleImageView.java | 153 + .../v4/widget/ContentLoadingProgressBar.java | 128 + .../support/v4/widget/CursorAdapter.java | 483 +++ .../support/v4/widget/CursorFilter.java | 71 + .../support/v4/widget/DrawerLayout.java | 1958 ++++++++++ .../v4/widget/DrawerLayoutCompatApi21.java | 94 + .../support/v4/widget/DrawerLayoutImpl.java | 26 + .../support/v4/widget/EdgeEffectCompat.java | 256 ++ .../v4/widget/EdgeEffectCompatIcs.java | 64 + .../v4/widget/EdgeEffectCompatLollipop.java | 27 + .../v4/widget/ExploreByTouchHelper.java | 725 ++++ .../v4/widget/ListPopupWindowCompat.java | 99 + .../widget/ListPopupWindowCompatKitKat.java | 30 + .../v4/widget/ListViewAutoScrollHelper.java | 94 + .../v4/widget/MaterialProgressDrawable.java | 792 ++++ .../support/v4/widget/NestedScrollView.java | 1862 +++++++++ .../support/v4/widget/PopupMenuCompat.java | 92 + .../v4/widget/PopupMenuCompatKitKat.java | 29 + .../support/v4/widget/PopupWindowCompat.java | 95 + .../v4/widget/PopupWindowCompatKitKat.java | 32 + .../v4/widget/ResourceCursorAdapter.java | 131 + .../support/v4/widget/ScrollerCompat.java | 484 +++ .../v4/widget/ScrollerCompatGingerbread.java | 90 + .../support/v4/widget/ScrollerCompatIcs.java | 28 + .../support/v4/widget/SearchViewCompat.java | 519 +++ .../v4/widget/SearchViewCompatHoneycomb.java | 122 + .../v4/widget/SearchViewCompatIcs.java | 53 + .../v4/widget/SimpleCursorAdapter.java | 398 ++ .../support/v4/widget/SlidingPaneLayout.java | 1644 ++++++++ .../src/android/support/v4/widget/Space.java | 84 + .../support/v4/widget/SwipeProgressBar.java | 276 ++ .../support/v4/widget/SwipeRefreshLayout.java | 956 +++++ .../support/v4/widget/TextViewCompat.java | 203 + .../v4/widget/TextViewCompatJbMr1.java | 49 + .../v4/widget/TextViewCompatJbMr2.java | 43 + .../support/v4/widget/ViewDragHelper.java | 1482 ++++++++ .../android/support/v4/widget/package.html | 8 + .../.classpath | 20 +- .../AndroidManifest.xml | 12 +- .../libs/android-support-v13.jar | Bin 1386017 -> 0 bytes .../libs/android-support-v4.jar | Bin 1157388 -> 0 bytes .../libs/android-support-v7-appcompat.jar | Bin 572410 -> 0 bytes .../res/anim/abc_fade_in.xml | 2 +- .../res/anim/abc_fade_out.xml | 2 +- .../res/anim/abc_grow_fade_in_from_bottom.xml | 2 +- .../res/anim/abc_popup_enter.xml | 2 +- .../res/anim/abc_popup_exit.xml | 2 +- .../anim/abc_shrink_fade_out_from_bottom.xml | 2 +- .../res/anim/abc_slide_in_bottom.xml | 1 + .../res/anim/abc_slide_in_top.xml | 2 +- .../res/anim/abc_slide_out_bottom.xml | 2 +- .../res/anim/abc_slide_out_top.xml | 2 +- ...ound_cache_hint_selector_material_dark.xml | 1 + ...und_cache_hint_selector_material_light.xml | 1 + ...ound_cache_hint_selector_material_dark.xml | 1 + ...und_cache_hint_selector_material_light.xml | 1 + ...rimary_text_disable_only_material_dark.xml | 1 + ...imary_text_disable_only_material_light.xml | 1 + .../color/abc_primary_text_material_dark.xml | 1 + .../color/abc_primary_text_material_light.xml | 1 + .../res/color/abc_search_url_text.xml | 2 +- .../abc_secondary_text_material_dark.xml | 1 + .../abc_secondary_text_material_light.xml | 1 + .../res/color/switch_thumb_material_dark.xml | 2 +- .../res/color/switch_thumb_material_light.xml | 2 +- .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 1204 -> 1105 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 1374 -> 1291 bytes .../abc_menu_hardkey_panel_mtrl_mult.9.png | Bin 675 -> 768 bytes .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 721 -> 699 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 843 -> 784 bytes .../abc_menu_hardkey_panel_mtrl_mult.9.png | Bin 449 -> 525 bytes .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 801 -> 0 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 939 -> 0 bytes .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 1797 -> 1601 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 2022 -> 1881 bytes .../abc_menu_hardkey_panel_mtrl_mult.9.png | Bin 951 -> 1077 bytes .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 2944 -> 2615 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 3373 -> 3305 bytes .../abc_menu_hardkey_panel_mtrl_mult.9.png | Bin 1577 -> 1764 bytes .../abc_btn_switch_to_on_mtrl_00001.9.png | Bin 2548 -> 2625 bytes .../abc_btn_switch_to_on_mtrl_00012.9.png | Bin 2963 -> 2984 bytes .../drawable/abc_btn_borderless_material.xml | 1 + .../res/drawable/abc_btn_check_material.xml | 2 +- .../drawable/abc_btn_default_mtrl_shape.xml | 1 + .../res/drawable/abc_btn_radio_material.xml | 2 +- .../abc_cab_background_internal_bg.xml | 2 +- .../abc_cab_background_top_material.xml | 1 + .../abc_dialog_material_background_dark.xml | 2 +- .../abc_dialog_material_background_light.xml | 2 +- .../res/drawable/abc_edit_text_material.xml | 1 + .../abc_item_background_holo_dark.xml | 1 + .../abc_item_background_holo_light.xml | 1 + ...lector_background_transition_holo_dark.xml | 1 + ...ector_background_transition_holo_light.xml | 1 + .../drawable/abc_list_selector_holo_dark.xml | 1 + .../drawable/abc_list_selector_holo_light.xml | 1 + .../drawable/abc_ratingbar_full_material.xml | 1 + ..._spinner_textfield_background_material.xml | 2 +- .../drawable/abc_switch_thumb_material.xml | 2 +- .../drawable/abc_tab_indicator_material.xml | 1 + .../abc_textfield_search_material.xml | 1 + .../layout-v11/abc_screen_content_include.xml | 26 - .../res/layout/abc_action_bar_title_item.xml | 1 + .../layout/abc_action_bar_up_container.xml | 1 + .../abc_action_bar_view_list_nav_layout.xml | 2 +- .../layout/abc_action_menu_item_layout.xml | 1 + .../res/layout/abc_action_menu_layout.xml | 1 + .../res/layout/abc_action_mode_bar.xml | 1 + .../abc_action_mode_close_item_material.xml | 2 +- .../res/layout/abc_activity_chooser_view.xml | 1 + .../abc_activity_chooser_view_list_item.xml | 2 +- .../res/layout/abc_alert_dialog_material.xml | 10 +- .../res/layout/abc_dialog_title_material.xml | 2 +- .../res/layout/abc_expanded_menu_layout.xml | 1 + .../layout/abc_list_menu_item_checkbox.xml | 1 + .../res/layout/abc_list_menu_item_icon.xml | 1 + .../res/layout/abc_list_menu_item_layout.xml | 1 + .../res/layout/abc_list_menu_item_radio.xml | 1 + .../res/layout/abc_popup_menu_item_layout.xml | 1 + .../res/layout/abc_screen_content_include.xml | 1 + .../res/layout/abc_screen_simple.xml | 1 + .../abc_screen_simple_overlay_action_mode.xml | 2 +- .../res/layout/abc_screen_toolbar.xml | 1 + .../abc_search_dropdown_item_icons_2line.xml | 1 + .../res/layout/abc_search_view.xml | 1 + .../res/layout/abc_select_dialog_material.xml | 2 +- .../res/layout/abc_simple_dropdown_hint.xml | 2 +- .../layout/select_dialog_item_material.xml | 1 + .../select_dialog_multichoice_material.xml | 1 + .../select_dialog_singlechoice_material.xml | 1 + .../support_simple_spinner_dropdown_item.xml | 2 +- .../res/values-af/strings.xml | 32 - .../res/values-am/strings.xml | 32 - .../res/values-ar/strings.xml | 32 - .../res/values-bg/strings.xml | 32 - .../res/values-bn-rBD/strings.xml | 32 - .../res/values-ca/strings.xml | 32 - .../res/values-cs/strings.xml | 32 - .../res/values-da/strings.xml | 32 - .../res/values-de/strings.xml | 32 - .../res/values-el/strings.xml | 32 - .../res/values-en-rGB/strings.xml | 32 - .../res/values-en-rIN/strings.xml | 32 - .../res/values-es-rUS/strings.xml | 32 - .../res/values-es/strings.xml | 32 - .../res/values-et-rEE/strings.xml | 32 - .../res/values-eu-rES/strings.xml | 32 - .../res/values-fa/strings.xml | 32 - .../res/values-fi/strings.xml | 32 - .../res/values-fr-rCA/strings.xml | 32 - .../res/values-fr/strings.xml | 32 - .../res/values-gl-rES/strings.xml | 32 - .../res/values-h720dp/dimens.xml | 20 - .../res/values-hdpi/styles_base.xml | 23 - .../res/values-hi/strings.xml | 32 - .../res/values-hr/strings.xml | 32 - .../res/values-hu/strings.xml | 32 - .../res/values-hy-rAM/strings.xml | 32 - .../res/values-in/strings.xml | 32 - .../res/values-is-rIS/strings.xml | 32 - .../res/values-it/strings.xml | 32 - .../res/values-iw/strings.xml | 32 - .../res/values-ja/strings.xml | 32 - .../res/values-ka-rGE/strings.xml | 32 - .../res/values-kk-rKZ/strings.xml | 32 - .../res/values-km-rKH/strings.xml | 32 - .../res/values-kn-rIN/strings.xml | 32 - .../res/values-ko/strings.xml | 32 - .../res/values-ky-rKG/strings.xml | 32 - .../res/values-land/bools.xml | 19 - .../res/values-land/config.xml | 18 - .../res/values-land/dimens.xml | 21 - .../res/values-land/dimens_material.xml | 27 - .../res/values-large/bools.xml | 19 - .../res/values-large/config.xml | 30 - .../res/values-large/dimens.xml | 38 - .../res/values-large/themes_base.xml | 25 - .../res/values-lo-rLA/strings.xml | 32 - .../res/values-lt/strings.xml | 32 - .../res/values-lv/strings.xml | 32 - .../res/values-mk-rMK/strings.xml | 34 - .../res/values-ml-rIN/strings.xml | 32 - .../res/values-mn-rMN/strings.xml | 32 - .../res/values-mr-rIN/strings.xml | 32 - .../res/values-ms-rMY/strings.xml | 32 - .../res/values-my-rMM/strings.xml | 32 - .../res/values-nb/strings.xml | 32 - .../res/values-ne-rNP/strings.xml | 32 - .../res/values-nl/strings.xml | 32 - .../res/values-pl/strings.xml | 32 - .../res/values-pt-rPT/strings.xml | 32 - .../res/values-pt/strings.xml | 32 - .../res/values-ro/strings.xml | 32 - .../res/values-ru/strings.xml | 32 - .../res/values-si-rLK/strings.xml | 32 - .../res/values-sk/strings.xml | 32 - .../res/values-sl/strings.xml | 32 - .../res/values-sr/strings.xml | 32 - .../res/values-sv/strings.xml | 32 - .../res/values-sw/strings.xml | 32 - .../res/values-sw600dp/dimens.xml | 40 - .../res/values-ta-rIN/strings.xml | 32 - .../res/values-te-rIN/strings.xml | 32 - .../res/values-th/strings.xml | 32 - .../res/values-tl/strings.xml | 32 - .../res/values-tr/strings.xml | 32 - .../res/values-uk/strings.xml | 32 - .../res/values-ur-rPK/strings.xml | 32 - .../res/values-uz-rUZ/strings.xml | 34 - .../res/values-v11/styles_base.xml | 40 - .../res/values-v11/styles_base_text.xml | 64 - .../res/values-v11/themes_base.xml | 149 - .../res/values-v12/themes_base.xml | 30 - .../res/values-v14/styles_base_text.xml | 26 - .../res/values-v14/themes_base.xml | 36 - .../res/values-v17/styles_rtl.xml | 72 - .../res/values-v21/styles_base.xml | 207 - .../res/values-v21/styles_base_text.xml | 64 - .../res/values-v21/themes_base.xml | 176 - .../res/values-vi/strings.xml | 32 - .../res/values-w360dp/dimens.xml | 22 - .../res/values-w480dp/bools.xml | 18 - .../res/values-w480dp/config.xml | 18 - .../res/values-w500dp/dimens.xml | 22 - .../res/values-w600dp/dimens.xml | 25 - .../res/values-w720dp/bools.xml | 19 - .../res/values-xlarge-land/dimens.xml | 22 - .../res/values-xlarge/bools.xml | 19 - .../res/values-xlarge/dimens.xml | 40 - .../res/values-zh-rCN/strings.xml | 32 - .../res/values-zh-rHK/strings.xml | 32 - .../res/values-zh-rTW/strings.xml | 32 - .../res/values-zu/strings.xml | 32 - .../res/values/attrs.xml | 927 ----- .../res/values/bools.xml | 24 - .../res/values/colors.xml | 22 - .../res/values/colors_material.xml | 99 - .../res/values/config.xml | 41 - .../res/values/dimens.xml | 100 - .../res/values/dimens_material.xml | 71 - .../res/values/ids.xml | 26 - .../res/values/strings.xml | 65 - .../res/values/styles.xml | 324 -- .../res/values/styles_base.xml | 484 --- .../res/values/styles_base_text.xml | 136 - .../res/values/styles_rtl.xml | 71 - .../res/values/themes.xml | 95 - .../res/values/themes_base.xml | 608 --- .../src/.readme | 0 .../.classpath | 20 +- .../AndroidManifest.xml | 16 +- .../libs/android-support-v7-recyclerview.jar | Bin 210201 -> 0 bytes .../res/.readme | 2 - .../src/.readme | 2 - .../de.psdev.licensesdialog/.classpath | 3 + .../project.properties | 1 - 655 files changed, 83018 insertions(+), 16777 deletions(-) delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java delete mode 100644 mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java create mode 100644 mobile/google-android-support-v13/.classpath create mode 100644 mobile/google-android-support-v13/.gitignore create mode 100644 mobile/google-android-support-v13/.project create mode 100644 mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs rename mobile/{google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml => google-android-support-v13/AndroidManifest.xml} (72%) create mode 100644 mobile/google-android-support-v13/libs/internal_impl-22.2.1.jar create mode 100644 mobile/google-android-support-v13/project.properties create mode 100644 mobile/google-android-support-v13/src/META-INF/MANIFEST.MF create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java create mode 100644 mobile/google-android-support-v13/src/android/support/v13/app/package.html create mode 100644 mobile/google-android-support-v4/.classpath create mode 100644 mobile/google-android-support-v4/.gitignore create mode 100644 mobile/google-android-support-v4/.project create mode 100644 mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs rename mobile/{google-android-support-v7-appcompat_lib/res/values-port/bools.xml => google-android-support-v4/AndroidManifest.xml} (72%) rename mobile/{google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml => google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl} (61%) create mode 100644 mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl create mode 100644 mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl create mode 100644 mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl create mode 100644 mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl create mode 100644 mobile/google-android-support-v4/libs/internal_impl-22.2.1.jar create mode 100644 mobile/google-android-support-v4/project.properties create mode 100644 mobile/google-android-support-v4/src/META-INF/MANIFEST.MF create mode 100644 mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentPagerAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransaction.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/app/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/Loader.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/ModernAsyncTask.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/database/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/os/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/Pair.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/Pools.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/util/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/view/package.html create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/Space.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java create mode 100644 mobile/google-android-support-v4/src/android/support/v4/widget/package.html delete mode 100644 mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar delete mode 100644 mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar delete mode 100644 mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-lo-rLA/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-lt/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-lv/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-mk-rMK/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ml-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-mn-rMN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-mr-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ms-rMY/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-my-rMM/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-nb/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ne-rNP/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-nl/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-pl/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-pt-rPT/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-pt/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ro/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ru/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-si-rLK/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sk/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sl/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sr/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sv/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sw/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ta-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-te-rIN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-th/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-tl/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-tr/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-uk/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-ur-rPK/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-uz-rUZ/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v12/themes_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v14/styles_base_text.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v17/styles_rtl.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base_text.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-v21/themes_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/config.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/styles_base.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/styles_base_text.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml delete mode 100644 mobile/google-android-support-v7-appcompat_lib/res/values/themes_base.xml mode change 100644 => 100755 mobile/google-android-support-v7-appcompat_lib/src/.readme delete mode 100644 mobile/google-android-support-v7-recyclerview_lib/libs/android-support-v7-recyclerview.jar delete mode 100644 mobile/google-android-support-v7-recyclerview_lib/res/.readme delete mode 100644 mobile/google-android-support-v7-recyclerview_lib/src/.readme 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/project.properties b/mobile/com.sap.sailing.racecommittee.app/project.properties index 5748d82c066..280985d63c7 100755 --- a/mobile/com.sap.sailing.racecommittee.app/project.properties +++ b/mobile/com.sap.sailing.racecommittee.app/project.properties @@ -17,3 +17,4 @@ 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/github-advrecyclerview_lib/.classpath b/mobile/github-advrecyclerview_lib/.classpath index 7bc01d9a9c6..9216bab0293 100644 --- a/mobile/github-advrecyclerview_lib/.classpath +++ b/mobile/github-advrecyclerview_lib/.classpath @@ -1,9 +1,12 @@ - - - - - - - - - + + + + + + + + + + + + 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/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java deleted file mode 100644 index 0230e2d17f3..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.animator; - -import android.support.v7.widget.RecyclerView; - -public abstract class BaseItemAnimator extends RecyclerView.ItemAnimator { - private ItemAnimatorListener mListener; - - /** - * The interface to be implemented by listeners to animation events from this - * ItemAnimator. This is used internally and is not intended for developers to - * create directly. - */ - public interface ItemAnimatorListener { - void onRemoveFinished(RecyclerView.ViewHolder item); - - void onAddFinished(RecyclerView.ViewHolder item); - - void onMoveFinished(RecyclerView.ViewHolder item); - - void onChangeFinished(RecyclerView.ViewHolder item); - } - - /** - * Internal only: - * Sets the listener that must be called when the animator is finished - * animating the item (or immediately if no animation happens). This is set - * internally and is not intended to be set by external code. - * - * @param listener The listener that must be called. - */ - public void setListener(ItemAnimatorListener listener) { - mListener = listener; - } - - @Override - public final void onAddStarting(RecyclerView.ViewHolder item) { - onAddStartingImpl(item); - } - - @Override - public final void onAddFinished(RecyclerView.ViewHolder item) { - onAddFinishedImpl(item); - - if (mListener != null) { - mListener.onAddFinished(item); - } - } - - @Override - public final void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) { - onChangeStartingItem(item, oldItem); - } - - @Override - public final void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { - onChangeFinishedImpl(item, oldItem); - - if (mListener != null) { - mListener.onChangeFinished(item); - } - } - - @Override - public final void onMoveStarting(RecyclerView.ViewHolder item) { - onMoveStartingImpl(item); - } - - @Override - public final void onMoveFinished(RecyclerView.ViewHolder item) { - onMoveFinishedImpl(item); - - if (mListener != null) { - mListener.onMoveFinished(item); - } - } - - @Override - public final void onRemoveStarting(RecyclerView.ViewHolder item) { - onRemoveStartingImpl(item); - } - - @Override - public final void onRemoveFinished(RecyclerView.ViewHolder item) { - onRemoveFinishedImpl(item); - - if (mListener != null) { - mListener.onRemoveFinished(item); - } - } - - @SuppressWarnings("EmptyMethod") - protected void onAddStartingImpl(RecyclerView.ViewHolder item) { - } - - @SuppressWarnings("EmptyMethod") - protected void onAddFinishedImpl(RecyclerView.ViewHolder item) { - } - - @SuppressWarnings("EmptyMethod") - protected void onChangeStartingItem(RecyclerView.ViewHolder item, boolean oldItem) { - } - - @SuppressWarnings("EmptyMethod") - protected void onChangeFinishedImpl(RecyclerView.ViewHolder item, boolean oldItem) { - } - - @SuppressWarnings("EmptyMethod") - protected void onMoveStartingImpl(RecyclerView.ViewHolder item) { - } - - @SuppressWarnings("EmptyMethod") - protected void onMoveFinishedImpl(RecyclerView.ViewHolder item) { - } - - @SuppressWarnings("EmptyMethod") - protected void onRemoveStartingImpl(RecyclerView.ViewHolder item) { - } - - @SuppressWarnings("EmptyMethod") - protected void onRemoveFinishedImpl(RecyclerView.ViewHolder item) { - } - - public boolean dispatchFinishedWhenDone() { - if (!isRunning()) { - dispatchAnimationsFinished(); - return true; - } else { - return false; - } - } - - public boolean debugLogEnabled() { - return false; - } -} 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 deleted file mode 100644 index f6f3aad8e2e..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * 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 langua - * ge governing permissions and - * limitations under the License. - */ - -package com.h6ah4i.android.widget.advrecyclerview.animator; - -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; - -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager; - -public abstract class GeneralItemAnimator extends BaseItemAnimator { - private static final String TAG = "ARVGeneralItemAnimator"; - - private boolean mDebug; - - private ItemRemoveAnimationManager mRemoveAnimationManager; - private ItemAddAnimationManager mAddAnimationsManager; - private ItemChangeAnimationManager mChangeAnimationsManager; - private ItemMoveAnimationManager mMoveAnimationsManager; - - GeneralItemAnimator() { - setup(); - } - - private void setup() { - onSetup(); - - if (mRemoveAnimationManager == null || - mAddAnimationsManager == null || - mChangeAnimationsManager == null || - mMoveAnimationsManager == null) { - throw new IllegalStateException("setup incomplete"); - } - } - - protected abstract void onSetup(); - - @Override - public void runPendingAnimations() { - if (!hasPendingAnimations()) { - return; - } - - onSchedulePendingAnimations(); - } - - @Override - public boolean animateRemove(final RecyclerView.ViewHolder holder) { - if (mDebug) { - Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); - } - - return mRemoveAnimationManager.addPendingAnimation(holder); - } - - @Override - public boolean animateAdd(final RecyclerView.ViewHolder holder) { - if (mDebug) { - Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); - } - - return mAddAnimationsManager.addPendingAnimation(holder); - } - - @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 + ")"); - } - - return mMoveAnimationsManager.addPendingAnimation(holder, fromX, fromY, toX, toY); - } - - @Override - public boolean animateChange( - 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 newId = (newHolder != null) ? Long.toString(newHolder.getItemId()) : "-"; - final String newPosition = (newHolder != null) ? Long.toString(newHolder.getPosition()) : "-"; - - Log.d(TAG, "animateChange(old.id = " + oldId + ", old.position = " + oldPosition + ", new.id = " + newId + ", new.position = " + newPosition - + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); - } - - return mChangeAnimationsManager.addPendingAnimation(oldHolder, newHolder, fromX, fromY, toX, toY); - } - - protected void cancelAnimations(RecyclerView.ViewHolder item) { - ViewCompat.animate(item.itemView).cancel(); - } - - @Override - public void endAnimation(RecyclerView.ViewHolder item) { - - // this will trigger end callback which should set properties to their target values. - cancelAnimations(item); - - mMoveAnimationsManager.endPendingAnimations(item); - mChangeAnimationsManager.endPendingAnimations(item); - mRemoveAnimationManager.endPendingAnimations(item); - mAddAnimationsManager.endPendingAnimations(item); - - mMoveAnimationsManager.endDeferredReadyAnimations(item); - mChangeAnimationsManager.endDeferredReadyAnimations(item); - mRemoveAnimationManager.endDeferredReadyAnimations(item); - mAddAnimationsManager.endDeferredReadyAnimations(item); - - // animations should be ended by the cancel above. - if (mRemoveAnimationManager.removeFromActive(item) && mDebug) { - throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [remove]"); - } - - if (mAddAnimationsManager.removeFromActive(item) && mDebug) { - throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [add]"); - } - - if (mChangeAnimationsManager.removeFromActive(item) && mDebug) { - throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [change]"); - } - - if (mMoveAnimationsManager.removeFromActive(item) && mDebug) { - throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [move]"); - } - - dispatchFinishedWhenDone(); - } - - @Override - public boolean isRunning() { - return (mRemoveAnimationManager.isRunning() || - mAddAnimationsManager.isRunning() || - mChangeAnimationsManager.isRunning() || - mMoveAnimationsManager.isRunning()); - } - - @Override - public void endAnimations() { - // end all pending animations - mMoveAnimationsManager.endAllPendingAnimations(); - mRemoveAnimationManager.endAllPendingAnimations(); - mAddAnimationsManager.endAllPendingAnimations(); - mChangeAnimationsManager.endAllPendingAnimations(); - - if (!isRunning()) { - return; - } - - // end all deferred animations - mMoveAnimationsManager.endAllDeferredReadyAnimations(); - mAddAnimationsManager.endAllDeferredReadyAnimations(); - mChangeAnimationsManager.endAllDeferredReadyAnimations(); - - // cancel all started animations - mRemoveAnimationManager.cancelAllStartedAnimations(); - mMoveAnimationsManager.cancelAllStartedAnimations(); - mAddAnimationsManager.cancelAllStartedAnimations(); - mChangeAnimationsManager.cancelAllStartedAnimations(); - - dispatchAnimationsFinished(); - } - - @Override - public boolean debugLogEnabled() { - return mDebug; - } - - @Override - public boolean dispatchFinishedWhenDone() { - if (mDebug && !isRunning()) { - Log.d(TAG, "dispatchFinishedWhenDone()"); - } - - return super.dispatchFinishedWhenDone(); - } - - protected boolean hasPendingAnimations() { - return (mRemoveAnimationManager.hasPending() || - mMoveAnimationsManager.hasPending() || - mChangeAnimationsManager.hasPending() || - mAddAnimationsManager.hasPending()); - } - - protected ItemRemoveAnimationManager getRemoveAnimationManager() { - return mRemoveAnimationManager; - } - - protected void setItemRemoveAnimationManager(ItemRemoveAnimationManager removeAnimationManager) { - mRemoveAnimationManager = removeAnimationManager; - } - - protected ItemAddAnimationManager getItemAddAnimationsManager() { - return mAddAnimationsManager; - } - - protected void setItemAddAnimationsManager(ItemAddAnimationManager addAnimationsManager) { - mAddAnimationsManager = addAnimationsManager; - } - - protected ItemChangeAnimationManager getItemChangeAnimationsManager() { - return mChangeAnimationsManager; - } - - protected void setItemChangeAnimationsManager(ItemChangeAnimationManager changeAnimationsManager) { - mChangeAnimationsManager = changeAnimationsManager; - } - - protected ItemMoveAnimationManager getItemMoveAnimationsManager() { - return mMoveAnimationsManager; - } - - protected void setItemMoveAnimationsManager(ItemMoveAnimationManager moveAnimationsManager) { - mMoveAnimationsManager = moveAnimationsManager; - } - - public boolean isDebug() { - return mDebug; - } - - public void setDebug(boolean debug) { - mDebug = debug; - } - - /** - * Schedule order and timing of pending animations. - * Override this method to custom animation order. - */ - protected void onSchedulePendingAnimations() { - schedulePendingAnimationsByDefaultRule(); - } - - @SuppressWarnings("UnnecessaryLocalVariable") - protected void schedulePendingAnimationsByDefaultRule() { - final boolean removalsPending = mRemoveAnimationManager.hasPending(); - final boolean movesPending = mMoveAnimationsManager.hasPending(); - final boolean changesPending = mChangeAnimationsManager.hasPending(); - final boolean additionsPending = mAddAnimationsManager.hasPending(); - - final long removeDuration = removalsPending ? getRemoveDuration() : 0; - final long moveDuration = movesPending ? getMoveDuration() : 0; - final long changeDuration = changesPending ? getChangeDuration() : 0; - - if (removalsPending) { - mRemoveAnimationManager.runPendingAnimations(false, 0); - } - - if (movesPending) { - final boolean deferred = removalsPending; - final long deferredDelay = removeDuration; - mMoveAnimationsManager.runPendingAnimations(deferred, deferredDelay); - } - - if (changesPending) { - final boolean deferred = removalsPending; - final long deferredDelay = removeDuration; - mChangeAnimationsManager.runPendingAnimations(deferred, deferredDelay); - } - - if (additionsPending) { - final boolean deferred = (removalsPending || movesPending || changesPending); - final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); - final long deferredDelay = (deferred) ? totalDelay : 0; - mAddAnimationsManager.runPendingAnimations(deferred, deferredDelay); - } - } -} 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 deleted file mode 100644 index fe6d8dbe6ca..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * 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.animator; - -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.AddAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ChangeAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.MoveAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.RemoveAnimationInfo; - -public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { - - @Override - protected void onSetup() { - setItemAddAnimationsManager(new DefaultItemAddAnimationManager(this)); - setItemRemoveAnimationManager(new DefaultItemRemoveAnimationManager(this)); - setItemChangeAnimationsManager(new DefaultItemChangeAnimationManager(this)); - setItemMoveAnimationsManager(new DefaultItemMoveAnimationManager(this)); - } - - @Override - protected void onSchedulePendingAnimations() { - schedulePendingAnimationsByDefaultRule(); - } - - /** - * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class) - */ - private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { - - public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(AddAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView); - - animator.alpha(1); - animator.setDuration(getDuration()); - - startActiveItemAnimation(info, info.holder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(AddAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - protected void onAnimationEndedBeforeStarted(AddAnimationInfo info, RecyclerView.ViewHolder item) { - ViewCompat.setAlpha(item.itemView, 1); - } - - @Override - protected void onAnimationCancel(AddAnimationInfo info, RecyclerView.ViewHolder item) { - ViewCompat.setAlpha(item.itemView, 1); - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder item) { - endAnimation(item); - - ViewCompat.setAlpha(item.itemView, 0); - - enqueuePendingAnimationInfo(new AddAnimationInfo(item)); - - return true; - } - } - - /** - * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class) - */ - private static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager { - - public DefaultItemRemoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(RemoveAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView); - - animator.setDuration(getDuration()); - animator.alpha(0); - - startActiveItemAnimation(info, info.holder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - } - - @Override - protected void onAnimationEndedBeforeStarted(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - } - - @Override - protected void onAnimationCancel(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder holder) { - endAnimation(holder); - - enqueuePendingAnimationInfo(new RemoveAnimationInfo(holder)); - return true; - } - } - - /** - * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class) - */ - private static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager { - public DefaultItemChangeAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.oldHolder.itemView); - - animator.setDuration(getDuration()); - animator.translationX(info.toX - info.fromX); - animator.translationY(info.toY - info.fromY); - animator.alpha(0); - - startActiveItemAnimation(info, info.oldHolder, animator); - } - - - @Override - protected void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.newHolder.itemView); - - animator.translationX(0); - animator.translationY(0); - animator.setDuration(getDuration()); - animator.alpha(1); - - startActiveItemAnimation(info, info.newHolder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } - - @Override - protected void onAnimationEndedBeforeStarted(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } - - @Override - protected void onAnimationCancel(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { - final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); - final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); - final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); - - endAnimation(oldHolder); - - final int deltaX = (int) (toX - fromX - prevTranslationX); - final int deltaY = (int) (toY - fromY - prevTranslationY); - - // recover prev translation state after ending animation - ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); - ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); - ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); - - if (newHolder != null && newHolder.itemView != null) { - // carry over translation values - endAnimation(newHolder); - ViewCompat.setTranslationX(newHolder.itemView, -deltaX); - ViewCompat.setTranslationY(newHolder.itemView, -deltaY); - ViewCompat.setAlpha(newHolder.itemView, 0); - } - - enqueuePendingAnimationInfo(new ChangeAnimationInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); - - return true; - } - } - - /** - * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class) - */ - private static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager { - - public DefaultItemMoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(MoveAnimationInfo info) { - final View view = info.holder.itemView; - final int deltaX = info.toX - info.fromX; - final int deltaY = info.toY - info.fromY; - - if (deltaX != 0) { - ViewCompat.animate(view).translationX(0); - } - if (deltaY != 0) { - ViewCompat.animate(view).translationY(0); - } - - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(view); - - animator.setDuration(getDuration()); - - startActiveItemAnimation(info, info.holder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - protected void onAnimationEndedBeforeStarted(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setTranslationY(view, 0); - ViewCompat.setTranslationX(view, 0); - } - - @Override - protected void onAnimationCancel(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - final int deltaX = info.toX - info.fromX; - final int deltaY = info.toY - info.fromY; - - if (deltaX != 0) { - ViewCompat.animate(view).translationX(0); - } - if (deltaY != 0) { - ViewCompat.animate(view).translationY(0); - } - - if (deltaX != 0) { - ViewCompat.setTranslationX(view, 0); - } - if (deltaY != 0) { - ViewCompat.setTranslationY(view, 0); - } - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY) { - final View view = item.itemView; - - fromX += ViewCompat.getTranslationX(item.itemView); - fromY += ViewCompat.getTranslationY(item.itemView); - - endAnimation(item); - - final int deltaX = toX - fromX; - final int deltaY = toY - fromY; - - final MoveAnimationInfo info = new MoveAnimationInfo(item, fromX, fromY, toX, toY); - - if (deltaX == 0 && deltaY == 0) { - dispatchFinished(info, info.holder); - info.clear(info.holder); - return false; - } - - if (deltaX != 0) { - ViewCompat.setTranslationX(view, -deltaX); - } - if (deltaY != 0) { - ViewCompat.setTranslationY(view, -deltaY); - } - - enqueuePendingAnimationInfo(info); - - return true; - } - } -} 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 deleted file mode 100644 index 6bf1514325d..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * 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.animator; - -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Interpolator; - -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.AddAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ChangeAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.MoveAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.animator.impl.RemoveAnimationInfo; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder; - -public class SwipeDismissItemAnimator extends GeneralItemAnimator { - - public static final Interpolator MOVE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); - - @Override - protected void onSetup() { - setItemAddAnimationsManager(new DefaultItemAddAnimationManager(this)); - setItemRemoveAnimationManager(new SwipeDismissItemRemoveAnimationManager(this)); - setItemChangeAnimationsManager(new SwipeDismissItemChangeAnimationManager(this)); - setItemMoveAnimationsManager(new SwipeDismissItemMoveAnimationManager(this)); - - setRemoveDuration(150); - setMoveDuration(150); - } - - @Override - protected void onSchedulePendingAnimations() { - schedulePendingAnimationsByDefaultRule(); - } - - @Override - protected void cancelAnimations(RecyclerView.ViewHolder item) { - super.cancelAnimations(item); - -// if (item instanceof SwipeableItemViewHolder) { -// final View containerView = ((SwipeableItemViewHolder) item).getSwipeableContainerView(); -// ViewCompat.animate(containerView).cancel(); -// } - } - - /** - * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class) - */ - private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { - - public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(AddAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView); - - animator.alpha(1); - animator.setDuration(getDuration()); - - startActiveItemAnimation(info, info.holder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(AddAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - protected void onAnimationEndedBeforeStarted(AddAnimationInfo info, RecyclerView.ViewHolder item) { - ViewCompat.setAlpha(item.itemView, 1); - } - - @Override - protected void onAnimationCancel(AddAnimationInfo info, RecyclerView.ViewHolder item) { - ViewCompat.setAlpha(item.itemView, 1); - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder item) { - endAnimation(item); - - ViewCompat.setAlpha(item.itemView, 0); - - enqueuePendingAnimationInfo(new AddAnimationInfo(item)); - - return true; - } - } - - /** - * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class) - */ - private static class SwipeDismissItemRemoveAnimationManager extends ItemRemoveAnimationManager { - private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateDecelerateInterpolator(); - - public SwipeDismissItemRemoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(RemoveAnimationInfo info) { - final ViewPropertyAnimatorCompat animator; - - if (isSwipeDismissed(info.holder)) { - final View view = info.holder.itemView; - animator = ViewCompat.animate(view); - animator.setDuration(getDuration()); - } else { - final View view = info.holder.itemView; - animator = ViewCompat.animate(view); - animator.setDuration(getDuration()); - animator.setInterpolator(DEFAULT_INTERPOLATOR); - animator.alpha(0); - } - - startActiveItemAnimation(info, info.holder, animator); - } - - private static boolean isSwipeDismissed(RecyclerView.ViewHolder item) { - if (!(item instanceof SwipeableItemViewHolder)) { - return false; - } - - final SwipeableItemViewHolder item2 = (SwipeableItemViewHolder) item; - final int result = item2.getSwipeResult(); - final int reaction = item2.getAfterSwipeReaction(); - - return ((result == RecyclerViewSwipeManager.RESULT_SWIPED_LEFT) || - (result == RecyclerViewSwipeManager.RESULT_SWIPED_RIGHT)) && - (reaction == RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_REMOVE_ITEM); - } - - private static boolean isSwipeDismissed(RemoveAnimationInfo info) { - return (info instanceof SwipeDismissRemoveAnimationInfo); - } - - @Override - protected void onAnimationEndedSuccessfully(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - - if (isSwipeDismissed(info)) { - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } else { - ViewCompat.setAlpha(view, 1); - } - } - - @Override - protected void onAnimationEndedBeforeStarted(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - - if (isSwipeDismissed(info)) { - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } else { - ViewCompat.setAlpha(view, 1); - } - } - - @Override - protected void onAnimationCancel(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder holder) { - if (isSwipeDismissed(holder)) { - final View itemView = holder.itemView; - final int prevItemX = (int) (ViewCompat.getTranslationX(itemView) + 0.5f); - final int prevItemY = (int) (ViewCompat.getTranslationY(itemView) + 0.5f); - - endAnimation(holder); - - ViewCompat.setTranslationX(itemView, prevItemX); - ViewCompat.setTranslationY(itemView, prevItemY); - - enqueuePendingAnimationInfo(new SwipeDismissRemoveAnimationInfo(holder)); - - return true; - } else { - endAnimation(holder); - - enqueuePendingAnimationInfo(new RemoveAnimationInfo(holder)); - - return true; - } - } - } - - private static class SwipeDismissRemoveAnimationInfo extends RemoveAnimationInfo { - public SwipeDismissRemoveAnimationInfo(RecyclerView.ViewHolder holder) { - super(holder); - } - } - - /** - * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class) - */ - private static class SwipeDismissItemChangeAnimationManager extends ItemChangeAnimationManager { - public SwipeDismissItemChangeAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.oldHolder.itemView); - - animator.setDuration(getDuration()); - animator.translationX(info.toX - info.fromX); - animator.translationY(info.toY - info.fromY); - animator.alpha(0); - - startActiveItemAnimation(info, info.oldHolder, animator); - } - - - @Override - protected void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.newHolder.itemView); - - animator.translationX(0); - animator.translationY(0); - animator.setDuration(getDuration()); - animator.alpha(1); - - startActiveItemAnimation(info, info.newHolder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } - - @Override - protected void onAnimationEndedBeforeStarted(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setAlpha(view, 1); - ViewCompat.setTranslationX(view, 0); - ViewCompat.setTranslationY(view, 0); - } - - @Override - protected void onAnimationCancel(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { - final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); - final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); - final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); - - endAnimation(oldHolder); - - final int deltaX = (int) (toX - fromX - prevTranslationX); - final int deltaY = (int) (toY - fromY - prevTranslationY); - - // recover prev translation state after ending animation - ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); - ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); - ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); - - if (newHolder != null && newHolder.itemView != null) { - // carry over translation values - endAnimation(newHolder); - ViewCompat.setTranslationX(newHolder.itemView, -deltaX); - ViewCompat.setTranslationY(newHolder.itemView, -deltaY); - ViewCompat.setAlpha(newHolder.itemView, 0); - } - - enqueuePendingAnimationInfo(new ChangeAnimationInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); - - return true; - } - } - - /** - * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class) - */ - private static class SwipeDismissItemMoveAnimationManager extends ItemMoveAnimationManager { - - public SwipeDismissItemMoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - protected void onCreateAnimation(MoveAnimationInfo info) { - final View view = info.holder.itemView; - final int deltaX = info.toX - info.fromX; - final int deltaY = info.toY - info.fromY; - - if (deltaX != 0) { - ViewCompat.animate(view).translationX(0); - } - if (deltaY != 0) { - ViewCompat.animate(view).translationY(0); - } - - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(view); - - animator.setDuration(getDuration()); - animator.setInterpolator(MOVE_INTERPOLATOR); - - startActiveItemAnimation(info, info.holder, animator); - } - - @Override - protected void onAnimationEndedSuccessfully(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - } - - @Override - protected void onAnimationEndedBeforeStarted(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - ViewCompat.setTranslationY(view, 0); - ViewCompat.setTranslationX(view, 0); - } - - @Override - protected void onAnimationCancel(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - final View view = item.itemView; - final int deltaX = info.toX - info.fromX; - final int deltaY = info.toY - info.fromY; - - if (deltaX != 0) { - ViewCompat.animate(view).translationX(0); - } - if (deltaY != 0) { - ViewCompat.animate(view).translationY(0); - } - - if (deltaX != 0) { - ViewCompat.setTranslationX(view, 0); - } - if (deltaY != 0) { - ViewCompat.setTranslationY(view, 0); - } - } - - @Override - public boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY) { - final View view = item.itemView; - - fromX += ViewCompat.getTranslationX(item.itemView); - fromY += ViewCompat.getTranslationY(item.itemView); - - endAnimation(item); - - final int deltaX = toX - fromX; - final int deltaY = toY - fromY; - - final MoveAnimationInfo info = new MoveAnimationInfo(item, fromX, fromY, toX, toY); - - if (deltaX == 0 && deltaY == 0) { - dispatchFinished(info, info.holder); - info.clear(info.holder); - return false; - } - - if (deltaX != 0) { - ViewCompat.setTranslationX(view, -deltaX); - } - if (deltaY != 0) { - ViewCompat.setTranslationY(view, -deltaY); - } - - enqueuePendingAnimationInfo(info); - - return true; - } - } -} 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 deleted file mode 100644 index faf644a0e62..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; - -public class AddAnimationInfo extends ItemAnimationInfo { - public RecyclerView.ViewHolder holder; - - public AddAnimationInfo(RecyclerView.ViewHolder holder) { - this.holder = holder; - } - - @Override - public RecyclerView.ViewHolder getAvailableViewHolder() { - return holder; - } - - @Override - public void clear(RecyclerView.ViewHolder item) { - if (holder == null) { - holder = null; - } - } - - @Override - public String toString() { - return "AddAnimationInfo{" + - "holder=" + holder + - '}'; - } -} 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 deleted file mode 100644 index 86c4050957d..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v4.view.ViewPropertyAnimatorListener; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator; - -import java.util.ArrayList; -import java.util.List; - -public abstract class BaseItemAnimationManager { - protected BaseItemAnimator mItemAnimator; - protected List mPending; - protected List> mDeferredReadySets; - protected List mActive; - - public BaseItemAnimationManager(BaseItemAnimator itemAnimator) { - mItemAnimator = itemAnimator; - mPending = new ArrayList<>(); - mActive = new ArrayList<>(); - mDeferredReadySets = new ArrayList<>(); - } - - protected final boolean debugLogEnabled() { - return mItemAnimator.debugLogEnabled(); - } - - public boolean hasPending() { - return !mPending.isEmpty(); - } - - public boolean isRunning() { - return !mPending.isEmpty() || !mActive.isEmpty() || !mDeferredReadySets.isEmpty(); - } - - public boolean removeFromActive(RecyclerView.ViewHolder item) { - return mActive.remove(item); - } - - public void cancelAllStartedAnimations() { - final List active = mActive; - for (int i = active.size() - 1; i >= 0; i--) { - final View view = active.get(i).itemView; - ViewCompat.animate(view).cancel(); - } - } - - public void runPendingAnimations(boolean deferred, long deferredDelay) { - final List ready = new ArrayList<>(); - - ready.addAll(mPending); - mPending.clear(); - - if (deferred) { - mDeferredReadySets.add(ready); - - final Runnable process = new Runnable() { - @Override - public void run() { - for (T info : ready) { - createAnimation(info); - } - ready.clear(); - mDeferredReadySets.remove(ready); - } - }; - - final View view = ready.get(0).getAvailableViewHolder().itemView; - ViewCompat.postOnAnimationDelayed(view, process, deferredDelay); - } else { - for (T info : ready) { - createAnimation(info); - } - ready.clear(); - } - } - - public abstract void dispatchStarting(T info, RecyclerView.ViewHolder item); - - public abstract void dispatchFinished(T info, RecyclerView.ViewHolder item); - - public abstract long getDuration(); - - public abstract void setDuration(long duration); - - public void endPendingAnimations(RecyclerView.ViewHolder item) { - final List pending = mPending; - - for (int i = pending.size() - 1; i >= 0; i--) { - final T info = pending.get(i); - - if (endNotStartedAnimation(info, item) && (item != null)) { - pending.remove(i); - } - } - - if (item == null) { - pending.clear(); - } - } - - public void endAllPendingAnimations() { - endPendingAnimations(null); - } - - public void endDeferredReadyAnimations(RecyclerView.ViewHolder item) { - for (int i = mDeferredReadySets.size() - 1; i >= 0; i--) { - final List ready = mDeferredReadySets.get(i); - - for (int j = ready.size() - 1; j >= 0; j--) { - final T info = ready.get(j); - - if (endNotStartedAnimation(info, item) && (item != null)) { - ready.remove(j); - } - } - - if (item == null) { - ready.clear(); - } - - if (ready.isEmpty()) { - mDeferredReadySets.remove(ready); - } - } - } - - public void endAllDeferredReadyAnimations() { - endDeferredReadyAnimations(null); - } - - /*package*/ void createAnimation(T info) { - onCreateAnimation(info); - } - - protected void endAnimation(RecyclerView.ViewHolder holder) { - mItemAnimator.endAnimation(holder); - } - - protected void dispatchFinishedWhenDone() { - mItemAnimator.dispatchFinishedWhenDone(); - } - - protected void enqueuePendingAnimationInfo(T info) { - if (info == null) { - throw new IllegalStateException("info is null"); - } - mPending.add(info); - } - - protected void startActiveItemAnimation(T info, RecyclerView.ViewHolder holder, ViewPropertyAnimatorCompat animator) { - animator.setListener(new BaseAnimatorListener(this, info, holder, animator)); - addActiveAnimationTarget(holder); - animator.start(); - } - - private void addActiveAnimationTarget(RecyclerView.ViewHolder item) { - if (item == null) { - throw new IllegalStateException("item is null"); - } - mActive.add(item); - } - - protected abstract void onCreateAnimation(final T info); - - protected abstract void onAnimationEndedSuccessfully(T info, RecyclerView.ViewHolder item); - - protected abstract void onAnimationEndedBeforeStarted(T info, RecyclerView.ViewHolder item); - - protected abstract void onAnimationCancel(T info, RecyclerView.ViewHolder item); - - protected abstract boolean endNotStartedAnimation(T info, RecyclerView.ViewHolder item); - - - protected static class BaseAnimatorListener implements ViewPropertyAnimatorListener { - private BaseItemAnimationManager mManager; - private ItemAnimationInfo mAnimationInfo; - private RecyclerView.ViewHolder mHolder; - private ViewPropertyAnimatorCompat mAnimator; - - public BaseAnimatorListener(BaseItemAnimationManager manager, ItemAnimationInfo info, RecyclerView.ViewHolder holder, ViewPropertyAnimatorCompat animator) { - mManager = manager; - mAnimationInfo = info; - mHolder = holder; - mAnimator = animator; - } - - @SuppressWarnings("unchecked") - @Override - public void onAnimationStart(View view) { - mManager.dispatchStarting(mAnimationInfo, mHolder); - } - - @SuppressWarnings("unchecked") - @Override - public void onAnimationEnd(View view) { - final BaseItemAnimationManager manager = mManager; - final ItemAnimationInfo info = mAnimationInfo; - final RecyclerView.ViewHolder holder = mHolder; - - mAnimator.setListener(null); - mManager = null; - mAnimationInfo = null; - mHolder = null; - mAnimator = null; - - manager.onAnimationEndedSuccessfully(info, holder); - manager.dispatchFinished(info, holder); - info.clear(holder); - manager.mActive.remove(holder); - manager.dispatchFinishedWhenDone(); - } - - @SuppressWarnings("unchecked") - @Override - public void onAnimationCancel(View view) { - mManager.onAnimationCancel(mAnimationInfo, mHolder); - } - } -} - 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 deleted file mode 100644 index dd8931a5578..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; - -public class ChangeAnimationInfo extends ItemAnimationInfo { - public RecyclerView.ViewHolder newHolder, oldHolder; - public int fromX, fromY, toX, toY; - - public ChangeAnimationInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - this.oldHolder = oldHolder; - this.newHolder = newHolder; - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - - @Override - public RecyclerView.ViewHolder getAvailableViewHolder() { - return (oldHolder != null) ? oldHolder : newHolder; - } - - @Override - public void clear(RecyclerView.ViewHolder item) { - if (oldHolder == item) { - oldHolder = null; - } - if (newHolder == item) { - newHolder = null; - } - if (oldHolder == null && newHolder == null) { - fromX = 0; - fromY = 0; - toX = 0; - toY = 0; - } - } - - @Override - public String toString() { - return "ChangeInfo{" + - ", oldHolder=" + oldHolder + - ", newHolder=" + newHolder + - ", fromX=" + fromX + - ", fromY=" + fromY + - ", toX=" + toX + - ", toY=" + toY + - '}'; - } -} 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 deleted file mode 100644 index 8e514f07c46..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; - -import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator; - -public abstract class ItemAddAnimationManager extends BaseItemAnimationManager { - private static final String TAG = "ARVItemAddAnimMgr"; - - public ItemAddAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - public long getDuration() { - return mItemAnimator.getAddDuration(); - } - - @Override - public void setDuration(long duration) { - mItemAnimator.setAddDuration(duration); - } - - @Override - public void dispatchStarting(AddAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchAddStarting(" + item + ")"); - } - mItemAnimator.dispatchAddStarting(item); - } - - @Override - public void dispatchFinished(AddAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchAddFinished(" + item + ")"); - } - mItemAnimator.dispatchAddFinished(item); - } - - @Override - protected boolean endNotStartedAnimation(AddAnimationInfo info, RecyclerView.ViewHolder item) { - if ((info.holder != null) && ((item == null) || (info.holder == item))) { - onAnimationEndedBeforeStarted(info, info.holder); - dispatchFinished(info, info.holder); - info.clear(info.holder); - return true; - } else { - return false; - } - } - - public abstract boolean addPendingAnimation(RecyclerView.ViewHolder item); -} 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 deleted file mode 100644 index a85e1582b8d..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; - -public abstract class ItemAnimationInfo { - public abstract RecyclerView.ViewHolder getAvailableViewHolder(); - - public abstract void clear(RecyclerView.ViewHolder holder); -} - 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 deleted file mode 100644 index 62620ee9d97..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; - -import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator; - -public abstract class ItemChangeAnimationManager extends BaseItemAnimationManager { - private static final String TAG = "ARVItemChangeAnimMgr"; - - public ItemChangeAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - public void dispatchStarting(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchChangeStarting(" + item + ")"); - } - mItemAnimator.dispatchChangeStarting(item, (item == info.oldHolder)); - } - - @Override - public void dispatchFinished(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchChangeFinished(" + item + ")"); - } - mItemAnimator.dispatchChangeFinished(item, (item == info.oldHolder)); - } - - @Override - public long getDuration() { - return mItemAnimator.getChangeDuration(); - } - - @Override - public void setDuration(long duration) { - mItemAnimator.setChangeDuration(duration); - } - - @Override - protected void onCreateAnimation(ChangeAnimationInfo info) { - if (info.oldHolder != null && info.oldHolder.itemView != null) { - onCreateChangeAnimationForOldItem(info); - } - - if (info.newHolder != null && info.newHolder.itemView != null) { - onCreateChangeAnimationForNewItem(info); - } - } - - @Override - protected boolean endNotStartedAnimation(ChangeAnimationInfo info, RecyclerView.ViewHolder item) { - if ((info.oldHolder != null) && ((item == null) || (info.oldHolder == item))) { - onAnimationEndedBeforeStarted(info, info.oldHolder); - dispatchFinished(info, info.oldHolder); - info.clear(info.oldHolder); - } - - if ((info.newHolder != null) && ((item == null) || (info.newHolder == item))) { - onAnimationEndedBeforeStarted(info, info.newHolder); - dispatchFinished(info, info.newHolder); - info.clear(info.newHolder); - } - - return (info.oldHolder == null && info.newHolder == null); - } - - protected abstract void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info); - - protected abstract void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info); - - public abstract boolean addPendingAnimation(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY); -} 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 deleted file mode 100644 index 1ba2bd33aa5..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; - -import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator; - -public abstract class ItemMoveAnimationManager extends BaseItemAnimationManager { - public static final String TAG = "ARVItemMoveAnimMgr"; - - public ItemMoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - public long getDuration() { - return mItemAnimator.getMoveDuration(); - } - - @Override - public void setDuration(long duration) { - mItemAnimator.setMoveDuration(duration); - } - - @Override - public void dispatchStarting(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchMoveStarting(" + item + ")"); - } - mItemAnimator.dispatchMoveStarting(item); - } - - @Override - public void dispatchFinished(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchMoveFinished(" + item + ")"); - } - mItemAnimator.dispatchMoveFinished(item); - } - - @Override - protected boolean endNotStartedAnimation(MoveAnimationInfo info, RecyclerView.ViewHolder item) { - if ((info.holder != null) && ((item == null) || (info.holder == item))) { - onAnimationEndedBeforeStarted(info, info.holder); - dispatchFinished(info, info.holder); - info.clear(info.holder); - return true; - } else { - return false; - } - } - - public abstract boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY); -} 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 deleted file mode 100644 index c5c3117b3f7..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; - -import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator; - -public abstract class ItemRemoveAnimationManager extends BaseItemAnimationManager { - private static final String TAG = "ARVItemRemoveAnimMgr"; - - public ItemRemoveAnimationManager(BaseItemAnimator itemAnimator) { - super(itemAnimator); - } - - @Override - public long getDuration() { - return mItemAnimator.getRemoveDuration(); - } - - @Override - public void setDuration(long duration) { - mItemAnimator.setRemoveDuration(duration); - } - - @Override - public void dispatchStarting(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchRemoveStarting(" + item + ")"); - } - mItemAnimator.dispatchRemoveStarting(item); - } - - @Override - public void dispatchFinished(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - if (debugLogEnabled()) { - Log.d(TAG, "dispatchRemoveFinished(" + item + ")"); - } - mItemAnimator.dispatchRemoveFinished(item); - } - - @Override - protected boolean endNotStartedAnimation(RemoveAnimationInfo info, RecyclerView.ViewHolder item) { - if ((info.holder != null) && ((item == null) || (info.holder == item))) { - onAnimationEndedBeforeStarted(info, info.holder); - dispatchFinished(info, info.holder); - info.clear(info.holder); - return true; - } else { - return false; - } - } - - public abstract boolean addPendingAnimation(RecyclerView.ViewHolder holder); -} 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 deleted file mode 100644 index e30e4fc9fa5..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; - -public class MoveAnimationInfo extends ItemAnimationInfo { - public RecyclerView.ViewHolder holder; - public int fromX, fromY, toX, toY; - - public MoveAnimationInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - this.holder = holder; - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - - @Override - public RecyclerView.ViewHolder getAvailableViewHolder() { - return holder; - } - - @Override - public void clear(RecyclerView.ViewHolder item) { - if (holder == item) { - holder = null; - } - } - - @Override - public String toString() { - return "MoveAnimationInfo{" + - "holder=" + holder + - ", fromX=" + fromX + - ", fromY=" + fromY + - ", toX=" + toX + - ", toY=" + toY + - '}'; - } -} \ No newline at end of file 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 deleted file mode 100644 index 131b6fcf976..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.animator.impl; - -import android.support.v7.widget.RecyclerView; - -public class RemoveAnimationInfo extends ItemAnimationInfo { - public RecyclerView.ViewHolder holder; - - public RemoveAnimationInfo(RecyclerView.ViewHolder holder) { - this.holder = holder; - } - - @Override - public RecyclerView.ViewHolder getAvailableViewHolder() { - return holder; - } - - @Override - public void clear(RecyclerView.ViewHolder item) { - if (holder == item) { - holder = null; - } - } - - @Override - public String toString() { - return "RemoveAnimationInfo{" + - "holder=" + holder + - '}'; - } -} 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 deleted file mode 100644 index d638bf6f3f1..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.decoration; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -/** - * Item decoration which draws drop shadow of each item views. - */ -public class ItemShadowDecorator extends RecyclerView.ItemDecoration { - private final NinePatchDrawable mShadowDrawable; - private final Rect mShadowPadding = new Rect(); - - /** - * Constructor. - * - * @param shadow 9-patch drawable used for drop shadow - */ - public ItemShadowDecorator(NinePatchDrawable shadow) { - mShadowDrawable = shadow; - mShadowDrawable.getPadding(mShadowPadding); - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - final int childCount = parent.getChildCount(); - - if (childCount == 0) { - return; - } - - int savedCount = c.save(Canvas.CLIP_SAVE_FLAG); - - c.clipRect( - parent.getLeft() + Math.max(0, parent.getPaddingLeft() - mShadowPadding.left), - parent.getTop()/* + Math.max(0, parent.getPaddingTop() - mShadowPadding.top)*/, - parent.getRight() - Math.max(0, parent.getPaddingRight() - mShadowPadding.right), - parent.getBottom()/* - Math.max(0, parent.getPaddingBottom() - mShadowPadding.bottom)*/); - - for (int i = 0; i < childCount; i++) { - final View child = parent.getChildAt(i); - - if (!shouldDrawDropShadow(child)) { - continue; - } - - final int tx = (int) (ViewCompat.getTranslationX(child) + 0.5f); - final int ty = (int) (ViewCompat.getTranslationY(child) + 0.5f); - - final int left = child.getLeft() - mShadowPadding.left; - final int right = child.getRight() + mShadowPadding.right; - final int top = child.getTop() - mShadowPadding.top; - final int bottom = child.getBottom() + mShadowPadding.bottom; - - mShadowDrawable.setBounds(left + tx, top + ty, right + tx, bottom + ty); - mShadowDrawable.draw(c); - } - - c.restoreToCount(savedCount); - - ViewCompat.postInvalidateOnAnimation(parent); - } - - private static boolean shouldDrawDropShadow(View child) { - if (child.getVisibility() != View.VISIBLE) { - return false; - } - if (ViewCompat.getAlpha(child) != 1.0f) { - return false; - } - - Drawable background = child.getBackground(); - if (background == null) { - return false; - } - - if (background instanceof ColorDrawable) { - //noinspection RedundantCast - if (((ColorDrawable) background).getAlpha() == 0) { - return false; - } - } - - return true; - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - outRect.set(0, 0, 0, 0); - } -} 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 deleted file mode 100644 index bde3277e695..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.decoration; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -/** - * Item decoration which draws item divider between each items. - */ -public class SimpleListDividerDecorator extends RecyclerView.ItemDecoration { - private final Drawable mDividerDrawable; - private final int mDividerHeight; - private boolean mOverlap; - - /** - * Constructor. - * - * @param divider divider drawable - * @param overlap whether the divider is drawn overlapped on bottom of the item. - */ - public SimpleListDividerDecorator(Drawable divider, boolean overlap) { - mDividerDrawable = divider; - mDividerHeight = mDividerDrawable.getIntrinsicHeight(); - mOverlap = overlap; - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - final int childCount = parent.getChildCount(); - final float yPositionThreshold = (mOverlap) ? 1.0f : (mDividerHeight + 1.0f); // [px] - final float zPositionThreshold = 1.0f; // [px] - - if (childCount == 0) { - return; - } - - int savedCount = c.save(Canvas.CLIP_SAVE_FLAG); - - c.clipRect( - parent.getLeft() + parent.getPaddingLeft(), - parent.getTop() + parent.getPaddingTop(), - parent.getRight() - parent.getPaddingRight(), - parent.getBottom() + parent.getPaddingBottom()); - - for (int i = 0; i < childCount - 1; i++) { - final View child = parent.getChildAt(i); - final View nextChild = parent.getChildAt(i + 1); - - if ((child.getVisibility() != View.VISIBLE) || - (nextChild.getVisibility() != View.VISIBLE)) { - continue; - } - - // check if the next item is placed at the bottom - final float childBottom = child.getBottom() + ViewCompat.getTranslationY(child); - final float nextChildTop = nextChild.getTop() + ViewCompat.getTranslationY(nextChild); - - if (!(Math.abs(nextChildTop - childBottom) < yPositionThreshold)) { - continue; - } - - // check if the next item is placed on the same plane - final float childZ = ViewCompat.getTranslationZ(child) + ViewCompat.getElevation(child); - final float nextChildZ = ViewCompat.getTranslationZ(nextChild) + ViewCompat.getElevation(nextChild); - - if (!(Math.abs(nextChildZ - childZ) < zPositionThreshold)) { - continue; - } - - final float childAlpha = ViewCompat.getAlpha(child); - final float nextChildAlpha = ViewCompat.getAlpha(nextChild); - - final int tx = (int) (ViewCompat.getTranslationX(child) + 0.5f); - final int ty = (int) (ViewCompat.getTranslationY(child) + 0.5f); - final int left = child.getLeft(); - final int right = child.getRight(); - final int top = child.getBottom(); - final int bottom = top + mDividerHeight; - - mDividerDrawable.setAlpha((int) ((0.5f * 255) * (childAlpha + nextChildAlpha) + 0.5f)); - mDividerDrawable.setBounds(left + tx, top + ty, right + tx, bottom + ty); - mDividerDrawable.draw(c); - } - - c.restoreToCount(savedCount); - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - if (mOverlap) { - outRect.set(0, 0, 0, 0); - } else { - outRect.set(0, 0, 0, mDividerHeight); - } - } -} 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 deleted file mode 100644 index 4acd9ec22d7..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.draggable; - -import android.os.Build; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v4.view.ViewPropertyAnimatorListener; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.animation.Interpolator; - -abstract class BaseDraggableItemDecorator extends RecyclerView.ItemDecoration { - - private static final int RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_DP = 2; - private static final int RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_MSEC = 20; - - private int mReturnToDefaultPositionDuration = 200; - private int mReturnToDefaultPositionAnimateThreshold; - private Interpolator mReturnToDefaultPositionInterpolator; - - protected RecyclerView mRecyclerView; - protected RecyclerView.ViewHolder mDraggingItem; - - public BaseDraggableItemDecorator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem) { - mRecyclerView = recyclerView; - mDraggingItem = draggingItem; - - final float displayDensity = recyclerView.getResources().getDisplayMetrics().density; - mReturnToDefaultPositionAnimateThreshold = (int) (RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_DP * displayDensity + 0.5f); - } - - - public void setReturnToDefaultPositionAnimationDuration(int duration) { - mReturnToDefaultPositionDuration = duration; - } - - public void setReturnToDefaultPositionAnimationInterpolator(Interpolator interpolator) { - mReturnToDefaultPositionInterpolator = interpolator; - } - - protected void moveToDefaultPosition(View targetView, boolean animate) { - final int curTranslationY = (int) (ViewCompat.getTranslationY(targetView)); - final int halfItemHeight = targetView.getHeight() / 2; - final float translationProportion = (halfItemHeight > 0) ? Math.abs((float) curTranslationY / halfItemHeight) : 0; - final float t = 1.0f - Math.min(translationProportion, 1.0f); - final int animDuration = (int) (mReturnToDefaultPositionDuration * (1.0f - (t * t)) + 0.5f); - - if (supportsViewPropertyAnimation() && animate && - (animDuration > RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_MSEC) && - (Math.abs(curTranslationY) > mReturnToDefaultPositionAnimateThreshold)) { - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(targetView); - animator.cancel(); - animator.setDuration(animDuration); - animator.setInterpolator(mReturnToDefaultPositionInterpolator); - animator.translationY(0.0f); - animator.setListener(new ViewPropertyAnimatorListener() { - @Override - public void onAnimationStart(View view) { - } - - @Override - public void onAnimationEnd(View view) { - animator.setListener(null); - ViewCompat.setTranslationY(view, 0); - - // invalidate explicitly to refresh other decorations - if (view.getParent() instanceof RecyclerView) { - ViewCompat.postInvalidateOnAnimation((RecyclerView) view.getParent()); - } - } - - @Override - public void onAnimationCancel(View view) { - } - }); - animator.start(); - } else { - ViewCompat.setTranslationY(targetView, 0); - } - } - - protected static void setItemTranslationY(RecyclerView rv, RecyclerView.ViewHolder holder, float y) { - final RecyclerView.ItemAnimator itemAnimator = rv.getItemAnimator(); - if (itemAnimator != null) { - itemAnimator.endAnimation(holder); - } - ViewCompat.setTranslationY(holder.itemView, y); - } - - private static boolean supportsViewPropertyAnimation() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; - } -} 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 deleted file mode 100644 index a3556622182..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.draggable; - - -import android.view.animation.Interpolator; - -public class BasicSwapTargetTranslationInterpolator implements Interpolator { - private final float mThreshold; - private final float mHalfValidRange; - private final float mInvValidRange; - - public BasicSwapTargetTranslationInterpolator() { - this(0.3f); - } - - public BasicSwapTargetTranslationInterpolator(float threshold) { - if (!(threshold >= 0 && threshold < 0.5f)) { - throw new IllegalArgumentException("Invalid threshold range: " + threshold); - } - final float validRange = 1.0f - 2 * threshold; - - mThreshold = threshold; - mHalfValidRange = validRange * 0.5f; - mInvValidRange = 1.0f / validRange; - } - - @Override - public float getInterpolation(float input) { - if (Math.abs(input - 0.5f) < mHalfValidRange) { - return (input - mThreshold) * mInvValidRange; - } else { - return (input < 0.5f) ? 0.0f : 1.0f; - } - } -} 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 deleted file mode 100644 index bf257edb9ca..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.draggable; - -import android.support.v7.widget.RecyclerView; - -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 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); - - /** - * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method returned true. - * - * @param holder The ViewHolder which is associated to item user is attempt to start dragging. - * - * @return null: no constraints (= new ItemDraggableRange(0, getItemCount() - 1)), - * otherwise: the range specified item can be drag-sortable. - */ - ItemDraggableRange onGetItemDraggableRange(T holder); - - /** - * Called when item is moved. Should apply the move operation result to data set. - * - * @param fromPosition Previous position of the item. - * @param toPosition New position of the item. - */ - void onMoveItem(int fromPosition, int toPosition); -} 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 deleted file mode 100644 index 370cdb3be01..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.draggable; - -/** - * Interface which provides required information for dragging item. - * - * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}. - */ -public interface DraggableItemViewHolder { - /** - * Sets the state flags value for dragging item - * - * @param flags Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_DRAGGING} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_ACTIVE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_IN_RANGE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_UPDATED} - */ - void setDragStateFlags(int flags); - - /** - * Gets the state flags value for dragging item - * - * @return Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_DRAGGING} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_ACTIVE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_IN_RANGE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_UPDATED} - */ - int getDragStateFlags(); -} 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 deleted file mode 100644 index f74d9515520..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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.draggable; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.ViewGroup; - -import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter; -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; - -class DraggableItemWrapperAdapter extends BaseWrapperAdapter { - 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 DEBUG_BYPASS_MOVE_OPERATION_MODE = false; - - private RecyclerViewDragDropManager mDragDropManager; - private DraggableItemAdapter mDraggableItemAdapter; - private RecyclerView.ViewHolder mDraggingItem; - private ItemDraggableRange mDraggableRange; - private long mDraggingItemId = RecyclerView.NO_ID; - private int mDraggingItemInitialPosition = RecyclerView.NO_POSITION; - private int mDraggingItemCurrentPosition = RecyclerView.NO_POSITION; - - public DraggableItemWrapperAdapter(RecyclerViewDragDropManager manager, RecyclerView.Adapter adapter) { - super(adapter); - - mDraggableItemAdapter = getDraggableItemAdapter(adapter); - if (getDraggableItemAdapter(adapter) == null) { - throw new IllegalArgumentException("adapter does not implement DraggableItemAdapter"); - } - - if (manager == null) { - throw new IllegalArgumentException("manager cannot be null"); - } - - mDragDropManager = manager; - } - - @Override - protected void onRelease() { - super.onRelease(); - mDraggingItem = null; - mDraggableItemAdapter = null; - mDragDropManager = null; - } - - @Override - public VH onCreateViewHolder(ViewGroup parent, int viewType) { - final VH holder = super.onCreateViewHolder(parent, viewType); - - if (holder instanceof DraggableItemViewHolder) { - ((DraggableItemViewHolder) holder).setDragStateFlags(STATE_FLAG_INITIAL_VALUE); - } - - return holder; - } - - @Override - public void onBindViewHolder(VH holder, int position) { - if (isDragging()) { - final long itemId = holder.getItemId(); - - final int origPosition = convertToOriginalPosition( - position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); - - if (itemId == mDraggingItemId && holder != mDraggingItem) { - Log.w(TAG, "an another view holder object for the currently dragging item is assigned"); - } - - int flags = RecyclerViewDragDropManager.STATE_FLAG_DRAGGING; - - if (itemId == mDraggingItemId) { - flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_ACTIVE; - } - if (mDraggableRange.checkInRange(position)) { - flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE; - } - - safeUpdateFlags(holder, flags); - super.onBindViewHolder(holder, origPosition); - } else { - safeUpdateFlags(holder, 0); - super.onBindViewHolder(holder, position); - } - } - - @Override - public long getItemId(int position) { - if (isDragging()) { - final int origPosition = convertToOriginalPosition( - position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); - return super.getItemId(origPosition); - } else { - return super.getItemId(position); - } - } - - @Override - public int getItemViewType(int position) { - if (isDragging()) { - final int origPosition = convertToOriginalPosition( - position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); - return super.getItemViewType(origPosition); - } else { - return super.getItemViewType(position); - } - } - - protected static int convertToOriginalPosition(int position, int dragInitial, int dragCurrent) { - if (dragInitial < 0 || dragCurrent < 0) { - // not dragging - return position; - } else { - if ((dragInitial == dragCurrent) || - ((position < dragInitial) && (position < dragCurrent)) || - (position > dragInitial) && (position > dragCurrent)) { - return position; - } else if (dragCurrent < dragInitial) { - if (position == dragCurrent) { - return dragInitial; - } else { - return position - 1; - } - } else { // if (dragCurrent > dragInitial) - if (position == dragCurrent) { - return dragInitial; - } else { - return position + 1; - } - } - } - } - - @Override - protected void onHandleWrappedAdapterChanged() { - if (shouldCancelDragOnDataUpdated()) { - cancelDrag(); - } else { - super.onHandleWrappedAdapterChanged(); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) { - if (shouldCancelDragOnDataUpdated()) { - cancelDrag(); - } else { - super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) { - if (shouldCancelDragOnDataUpdated()) { - cancelDrag(); - } else { - super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) { - if (shouldCancelDragOnDataUpdated()) { - cancelDrag(); - } else { - super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) { - if (shouldCancelDragOnDataUpdated()) { - cancelDrag(); - } else { - super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount); - } - } - - private boolean shouldCancelDragOnDataUpdated() { - //noinspection SimplifiableIfStatement - if (DEBUG_BYPASS_MOVE_OPERATION_MODE) { - return false; - } - return isDragging(); - } - - private void cancelDrag() { - if (mDragDropManager != null) { - mDragDropManager.cancelDrag(); - } - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void onDragItemStarted(RecyclerView.ViewHolder holder, ItemDraggableRange range) { - if (LOCAL_LOGD) { - Log.d(TAG, "onDragItemStarted(holder = " + holder + ")"); - } - - if (DEBUG_BYPASS_MOVE_OPERATION_MODE) { - return; - } - - if (holder.getItemId() == RecyclerView.NO_ID) { - throw new IllegalStateException("dragging target must provides valid ID"); - } - - mDraggingItemInitialPosition = holder.getPosition(); - mDraggingItemCurrentPosition = mDraggingItemInitialPosition; - mDraggingItem = holder; - mDraggingItemId = holder.getItemId(); - mDraggableRange = range; - - notifyDataSetChanged(); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void onDragItemFinished(RecyclerView.ViewHolder holder, boolean result) { - if (LOCAL_LOGD) { - Log.d(TAG, "onDragItemFinished(holder = " + holder + ", result = " + result + ")"); - } - - if (holder != mDraggingItem) { - throw new IllegalStateException("onDragItemFinished() - may be a bug (mDraggingItem != holder)"); - } - - if (DEBUG_BYPASS_MOVE_OPERATION_MODE) { - return; - } - - if (result && (mDraggingItemCurrentPosition != mDraggingItemInitialPosition)) { - // apply to wrapped adapter - ((DraggableItemAdapter) getWrappedAdapter()).onMoveItem( - mDraggingItemInitialPosition, mDraggingItemCurrentPosition); - } - - mDraggingItemInitialPosition = RecyclerView.NO_POSITION; - mDraggingItemCurrentPosition = RecyclerView.NO_POSITION; - mDraggableRange = null; - mDraggingItemId = RecyclerView.NO_ID; - mDraggingItem = null; - - notifyDataSetChanged(); - } - - @Override - 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(); - } - } - } - - super.onViewRecycled(holder); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ - @SuppressWarnings("unchecked") - boolean canStartDrag(RecyclerView.ViewHolder holder, int x, int y) { - if (LOCAL_LOGV) { - Log.v(TAG, "canStartDrag(holder = " + holder + ", x = " + x + ", y = " + y + ")"); - } - return mDraggableItemAdapter.onCheckCanStartDrag(holder, x, y); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ - @SuppressWarnings("unchecked") - ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder) { - if (LOCAL_LOGV) { - Log.v(TAG, "getItemDraggableRange(holder = " + holder + ")"); - } - return mDraggableItemAdapter.onGetItemDraggableRange(holder); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void moveItem(int fromPosition, int toPosition) { - if (LOCAL_LOGD) { - Log.d(TAG, "onMoveItem(fromPosition = " + fromPosition + ", toPosition = " + toPosition + ")"); - } - - if (DEBUG_BYPASS_MOVE_OPERATION_MODE) { - mDraggableItemAdapter.onMoveItem(fromPosition, toPosition); - return; - } - - final int origFromPosition = convertToOriginalPosition( - fromPosition, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); - - if (origFromPosition != mDraggingItemInitialPosition) { - throw new IllegalStateException( - "onMoveItem() - may be a bug or has duplicate IDs --- " + - "mDraggingItemInitialPosition = " + mDraggingItemInitialPosition + ", " + - "mDraggingItemCurrentPosition = " + mDraggingItemCurrentPosition + ", " + - "origFromPosition = " + origFromPosition + ", " + - "fromPosition = " + fromPosition + ", " + - "toPosition = " + toPosition); - } - - mDraggingItemCurrentPosition = toPosition; - - // NOTE: - // Don't move items in wrapped adapter here. - - // notify to observers - notifyItemMoved(fromPosition, toPosition); - } - - protected boolean isDragging() { - return mDragDropManager.isDragging(); - } - - private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) { - if (!(holder instanceof DraggableItemViewHolder)) { - return; - } - - final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder; - - final int curFlags = holder2.getDragStateFlags(); - final int mask = ~RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED; - - // append UPDATED flag - if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) { - flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED; - } - - ((DraggableItemViewHolder) holder).setDragStateFlags(flags); - } - - private static DraggableItemAdapter getDraggableItemAdapter(RecyclerView.Adapter adapter) { - return WrapperAdapterUtils.findWrappedAdapter(adapter, DraggableItemAdapter.class); - } -} 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 deleted file mode 100644 index 00e6bb4d1cd..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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.draggable; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.NinePatchDrawable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.MotionEvent; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; - -class DraggingItemDecorator extends BaseDraggableItemDecorator { - @SuppressWarnings("unused") - private static final String TAG = "DraggingItemDecorator"; - - private int mGrabbedPositionY; - private int mTranslationY; - private int mRecyclerViewPaddingLeft; - private Bitmap mDraggingItemImage; - private int mTranslationTopLimit; - private int mTranslationBottomLimit; - private int mGrabbedItemHeight; - private int mTouchPositionY; - private NinePatchDrawable mShadowDrawable; - private Rect mShadowPadding = new Rect(); - private Rect mDraggingItemMargins = new Rect(); - private boolean mStarted; - private boolean mIsScrolling; - private ItemDraggableRange mRange; - - public DraggingItemDecorator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem, ItemDraggableRange range) { - super(recyclerView, draggingItem); - mRange = range; - CustomRecyclerViewUtils.getLayoutMargins(mDraggingItem.itemView, mDraggingItemMargins); - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - // NOTE: - // On lollipop or later, View has Z-axis property and no needed to draw the dragging view manually. - // However, if the RecyclerView has any other decorations or RecyclerView is in scrolling state, - // need to draw it to avoid visual corruptions. - if (mDraggingItemImage != null) { - final float left = mRecyclerViewPaddingLeft + mDraggingItemMargins.left - mShadowPadding.left; - final float top = /*mDraggingItemMargins.top +*/ mTranslationY - mShadowPadding.top; - c.drawBitmap(mDraggingItemImage, left, top, null); - } - } - - public void start(MotionEvent e, float grabbedPositionY) { - if (mStarted) { - return; - } - - final View itemView = mDraggingItem.itemView; - - mGrabbedPositionY = (int) (grabbedPositionY + 0.5f); - - // draw the grabbed item on bitmap - mDraggingItemImage = createDraggingItemImage(itemView, mShadowDrawable); - - mGrabbedItemHeight = itemView.getHeight(); - mTranslationTopLimit = mRecyclerView.getPaddingTop(); - mRecyclerViewPaddingLeft = mRecyclerView.getPaddingLeft(); - - // hide - itemView.setVisibility(View.INVISIBLE); - - update(e); - - mRecyclerView.addItemDecoration(this); - - mStarted = true; - } - - public void finish(boolean animate) { - if (mStarted) { - mRecyclerView.removeItemDecoration(this); - } - - final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); - if (itemAnimator != null) { - itemAnimator.endAnimations(); - } - mRecyclerView.stopScroll(); - - // return to default position - updateDraggingItemPosition(mTranslationY); - moveToDefaultPosition(mDraggingItem.itemView, animate); - - // show - mDraggingItem.itemView.setVisibility(View.VISIBLE); - mDraggingItem = null; - - if (mDraggingItemImage != null) { - mDraggingItemImage.recycle(); - mDraggingItemImage = null; - } - - mRange = null; - mGrabbedPositionY = 0; - mTranslationY = 0; - mTranslationTopLimit = 0; - mTranslationBottomLimit = 0; - mRecyclerViewPaddingLeft = 0; - mGrabbedItemHeight = 0; - mTouchPositionY = 0; - mStarted = false; - } - - public void update(MotionEvent e) { - mTouchPositionY = (int) (e.getY() + 0.5f); - refresh(); - } - - public void refresh() { - updateTranslationOffset(); - updateDraggingItemPosition(mTranslationY); - - ViewCompat.postInvalidateOnAnimation(mRecyclerView); - } - - public void setShadowDrawable(NinePatchDrawable shadowDrawable) { - mShadowDrawable = shadowDrawable; - - if (mShadowDrawable != null) { - mShadowDrawable.getPadding(mShadowPadding); - } - } - - public int getDraggingItemTranslationY() { - return mTranslationY; - } - - private void updateTranslationOffset() { - final int childCount = mRecyclerView.getChildCount(); - if (childCount > 0) { - mTranslationTopLimit = mRecyclerView.getPaddingTop(); - mTranslationBottomLimit = Math.max(0, (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() - mGrabbedItemHeight)); - - if (!mIsScrolling) { - final int firstVisiblePosition = CustomRecyclerViewUtils.findFirstVisibleItemPosition(mRecyclerView); - final int lastVisiblePosition = CustomRecyclerViewUtils.findLastVisibleItemPosition(mRecyclerView); - final View topChild = findRangeFirstItem(mRecyclerView, mRange, firstVisiblePosition, lastVisiblePosition); - final View bottomChild = findRangeLastItem(mRecyclerView, mRange, firstVisiblePosition, lastVisiblePosition); - - if (topChild != null) { - mTranslationTopLimit = Math.min(mTranslationBottomLimit, topChild.getTop()); - } - - if (bottomChild != null) { - mTranslationBottomLimit = Math.min(mTranslationBottomLimit, bottomChild.getTop()); - } - } - } else { - mTranslationBottomLimit = mTranslationTopLimit = mRecyclerView.getPaddingTop(); - } - - mTranslationY = mTouchPositionY - mGrabbedPositionY; - mTranslationY = Math.min(Math.max(mTranslationY, mTranslationTopLimit), mTranslationBottomLimit); - } - - - private Bitmap createDraggingItemImage(View v, NinePatchDrawable shadow) { - int width = v.getWidth() + mShadowPadding.left + mShadowPadding.right; - int height = v.getHeight() + mShadowPadding.top + mShadowPadding.bottom; - - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - - final Canvas canvas = new Canvas(bitmap); - - if (shadow != null) { - shadow.setBounds(0, 0, width, height); - shadow.draw(canvas); - } - - final int savedCount = canvas.save(Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG); - // NOTE: Explicitly set clipping rect. This is required on Gingerbread. - canvas.clipRect(mShadowPadding.left, mShadowPadding.top, width - mShadowPadding.right, height - mShadowPadding.bottom); - canvas.translate(mShadowPadding.left, mShadowPadding.top); - v.draw(canvas); - canvas.restoreToCount(savedCount); - - return bitmap; - } - - 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()); - } - - public void setIsScrolling(boolean isScrolling) { - if (mIsScrolling == isScrolling) { - return; - } - - mIsScrolling = isScrolling; - - } - - public int getTranslatedItemPositionTop() { - return mTranslationY; - } - - public int getTranslatedItemPositionBottom() { - return mTranslationY + mGrabbedItemHeight; - } - - - private static View findRangeFirstItem(RecyclerView rv, ItemDraggableRange range, int firstVisiblePosition, int lastVisiblePosition) { - if (firstVisiblePosition == RecyclerView.NO_POSITION || lastVisiblePosition == RecyclerView.NO_POSITION) { - return null; - } - - View v = null; - - final int childCount = rv.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View v2 = rv.getChildAt(i); - final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); - - if (vh != null) { - final int position = vh.getPosition(); - - if ((position >= firstVisiblePosition) && - (position <= lastVisiblePosition) && - range.checkInRange(position)) { - v = v2; - break; - } - - } - } - - return v; - } - - private static View findRangeLastItem(RecyclerView rv, ItemDraggableRange range, int firstVisiblePosition, int lastVisiblePosition) { - if (firstVisiblePosition == RecyclerView.NO_POSITION || lastVisiblePosition == RecyclerView.NO_POSITION) { - return null; - } - - View v = null; - - final int childCount = rv.getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - final View v2 = rv.getChildAt(i); - final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); - - if (vh != null) { - final int position = vh.getPosition(); - - if ((position >= firstVisiblePosition) && - (position <= lastVisiblePosition) && - range.checkInRange(position)) { - v = v2; - break; - } - } - } - - return v; - } -} \ 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 deleted file mode 100644 index 372a595aec8..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.draggable; - -import android.graphics.Canvas; -import android.support.v4.view.ViewCompat; -import android.support.v4.widget.EdgeEffectCompat; -import android.support.v7.widget.RecyclerView; - -public class EdgeEffectDecorator extends RecyclerView.ItemDecoration { - private RecyclerView mRecyclerView; - private EdgeEffectCompat mTopGlow; - private EdgeEffectCompat mBottomGlow; - private boolean mStarted; - - public EdgeEffectDecorator(RecyclerView recyclerView) { - mRecyclerView = recyclerView; - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - boolean needsInvalidate = false; - - if (mTopGlow != null && !mTopGlow.isFinished()) { - final int restore = c.save(); - if (getClipToPadding(parent)) { - c.translate(parent.getPaddingLeft(), parent.getPaddingTop()); - } - //noinspection ConstantConditions - needsInvalidate |= mTopGlow.draw(c); - c.restoreToCount(restore); - } - - if (mBottomGlow != null && !mBottomGlow.isFinished()) { - final int restore = c.save(); - c.rotate(180); - if (getClipToPadding(parent)) { - c.translate(-parent.getWidth() + parent.getPaddingRight(), -parent.getHeight() + parent.getPaddingBottom()); - } else { - c.translate(-parent.getWidth(), -parent.getHeight()); - } - needsInvalidate |= mBottomGlow.draw(c); - c.restoreToCount(restore); - } - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(parent); - } - } - - public void start() { - if (mStarted) { - return; - } - mRecyclerView.addItemDecoration(this); - mStarted = true; - } - - public void finish() { - if (mStarted) { - mRecyclerView.removeItemDecoration(this); - } - releaseBothGlows(); - mRecyclerView = null; - mStarted = false; - } - - public void pullTopGlow(float deltaDistance) { - ensureTopGlow(mRecyclerView); - - if (mTopGlow.onPull(deltaDistance)) { - ViewCompat.postInvalidateOnAnimation(mRecyclerView); - } - } - - public void pullBottom(float deltaDistance) { - ensureBottomGlow(mRecyclerView); - mBottomGlow.onPull(deltaDistance); - - if (mBottomGlow.onPull(deltaDistance)) { - ViewCompat.postInvalidateOnAnimation(mRecyclerView); - } - } - - public void releaseBothGlows() { - boolean needsInvalidate = false; - - if (mTopGlow != null) { - //noinspection ConstantConditions - needsInvalidate |= mTopGlow.onRelease(); - } - - if (mBottomGlow != null) { - needsInvalidate |= mBottomGlow.onRelease(); - } - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(mRecyclerView); - } - } - - 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()); - } - } - - private void ensureBottomGlow(RecyclerView rv) { - if (mBottomGlow != null) { - return; - } - mBottomGlow = new EdgeEffectCompat(rv.getContext()); - if (getClipToPadding(rv)) { - mBottomGlow.setSize( - rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(), - rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom()); - } else { - mBottomGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight()); - } - } - - private static boolean getClipToPadding(RecyclerView rv) { - return rv.getLayoutManager().getClipToPadding(); - } - - public void reorderToTop() { - if (mStarted) { - mRecyclerView.removeItemDecoration(this); - mRecyclerView.addItemDecoration(this); - } - } -} 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 deleted file mode 100644 index 2625cf5120d..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.draggable; - -public class ItemDraggableRange { - private final int mStart; - private final int mEnd; - - public ItemDraggableRange(int start, int end) { - if (!(start <= end)) { - throw new IllegalArgumentException("end position (= " + end + ") is smaller than start position (=" + start + ")"); - } - - mStart = start; - mEnd = end; - } - - public int getStart() { - return mStart; - } - - public int getEnd() { - return mEnd; - } - - public boolean checkInRange(int position) { - return ((position >= mStart) && (position <= mEnd)); - } - - @Override - public String toString() { - return "ItemDraggableRange{" + - "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 deleted file mode 100644 index 41667008824..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java +++ /dev/null @@ -1,1110 +0,0 @@ -/* - * 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.draggable; - -import android.graphics.Rect; -import android.graphics.drawable.NinePatchDrawable; -import android.os.Build; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; - -import com.h6ah4i.android.widget.advrecyclerview.event.RecyclerViewOnScrollEventDistributor; -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; - -import java.lang.ref.WeakReference; - -/** - * Provides item drag & drop operation for {@link android.support.v7.widget.RecyclerView} - */ -@SuppressWarnings("PointlessBitwiseExpression") -public class RecyclerViewDragDropManager { - private static final String TAG = "ARVDragDropManager"; - - /** - * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods. - * Indicates that currently performing dragging. - */ - public static final int STATE_FLAG_DRAGGING = (1 << 0); - - /** - * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods. - * Indicates that this item is being dragged. - */ - public static final int STATE_FLAG_IS_ACTIVE = (1 << 1); - - /** - * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods. - * Indicates that this item is in the range of drag-sortable items - */ - public static final int STATE_FLAG_IS_IN_RANGE = (1 << 2); - - /** - * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods. - * If this flag is set, some other flags are changed and require to apply. - */ - public static final int STATE_FLAG_IS_UPDATED = (1 << 31); - - // --- - - /** - * Default interpolator used for "swap target transition" - */ - public static final Interpolator DEFAULT_SWAP_TARGET_TRANSITION_INTERPOLATOR = new BasicSwapTargetTranslationInterpolator(); - - - /** - * Default interpolator used for "item settle back into place" animation - */ - public static final Interpolator DEFAULT_ITEM_SETTLE_BACK_INTO_PLACE_ANIMATION_INTERPOLATOR = new DecelerateInterpolator(); - - // --- - - 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); - - private static final boolean LOCAL_LOGV = false; - private static final boolean LOCAL_LOGD = false; - - 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; - private Interpolator mSwapTargetTranslationInterpolator = DEFAULT_SWAP_TARGET_TRANSITION_INTERPOLATOR; - private ScrollOnDraggingProcessRunnable mScrollOnDraggingProcess; - private boolean mScrollEventRegisteredToDistributor; - - private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; - private RecyclerView.OnScrollListener mInternalUseOnScrollListener; - - private EdgeEffectDecorator mEdgeEffectDecorator; - private NinePatchDrawable mShadowDrawable; - - private float mDisplayDensity; - private int mTouchSlop; - private int mScrollTouchSlop; - private int mInitialTouchY; - private long mInitialTouchItemId = RecyclerView.NO_ID; - - private boolean mInScrollByMethod; - private int mActualScrollByAmount; - - private Rect mTmpRect1 = new Rect(); - - private Runnable mDeferredCancelProcess; - private int mItemSettleBackIntoPlaceAnimationDuration = 200; - private Interpolator mItemSettleBackIntoPlaceAnimationInterpolator = DEFAULT_ITEM_SETTLE_BACK_INTO_PLACE_ANIMATION_INTERPOLATOR; - - // these fields are only valid while dragging - private DraggableItemWrapperAdapter mAdapter; - private long mDraggingItemId = RecyclerView.NO_ID; - private RecyclerView.ViewHolder mDraggingItem; - private Rect mDraggingItemMargins = new Rect(); - private DraggingItemDecorator mDraggingItemDecorator; - private SwapTargetItemOperator mSwapTargetItemOperator; - private int mLastTouchY; - private int mDragStartTouchY; - private int mDragMinTouchY; - private int mDragMaxTouchY; - private int mScrollDirMask = SCROLL_DIR_NONE; - private int mGrabbedPositionY; - private int mGrabbedItemHeight; - private int mOrigOverScrollMode; - private ItemDraggableRange mDraggableRange; - - /** - * Constructor. - */ - public RecyclerViewDragDropManager() { - mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - return RecyclerViewDragDropManager.this.onInterceptTouchEvent(rv, e); - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - RecyclerViewDragDropManager.this.onTouchEvent(rv, e); - } - }; - - mInternalUseOnScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - RecyclerViewDragDropManager.this.onScrollStateChanged(recyclerView, newState); - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - RecyclerViewDragDropManager.this.onScrolled(recyclerView, dx, dy); - } - }; - - mScrollOnDraggingProcess = new ScrollOnDraggingProcessRunnable(this); - } - - /** - * Create wrapped adapter. - * - * @param adapter The target adapter. - * - * @return Wrapped adapter which is associated to this {@link RecyclerViewDragDropManager} instance. - */ - @SuppressWarnings("unchecked") - public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) { - if (mAdapter != null) { - throw new IllegalStateException("already have a wrapped adapter"); - } - - mAdapter = new DraggableItemWrapperAdapter(this, adapter); - - return mAdapter; - } - - /** - * 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. - * - * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set - * 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 - */ - public void attachRecyclerView(RecyclerView rv) { - attachRecyclerView(rv, null); - } - - /** - * Attaches {@link android.support.v7.widget.RecyclerView} instance. - * - * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set - * 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 scrollEventDistributor The distributor for {@link android.support.v7.widget.RecyclerView.OnScrollListener} event - */ - public void attachRecyclerView(RecyclerView rv, RecyclerViewOnScrollEventDistributor scrollEventDistributor) { - if (rv == null) { - throw new IllegalArgumentException("RecyclerView cannot be null"); - } - - if (isReleased()) { - throw new IllegalStateException("Accessing released object"); - } - - if (mRecyclerView != null) { - throw new IllegalStateException("RecyclerView instance has already been set"); - } - - if (mAdapter == null || getDraggableItemWrapperAdapter(rv) != mAdapter) { - throw new IllegalStateException("adapter is not set properly"); - } - - if (scrollEventDistributor != null) { - final RecyclerView rv2 = scrollEventDistributor.getRecyclerView(); - - if (rv2 != null && rv2 != rv) { - throw new IllegalArgumentException("The scroll event distributor attached to different RecyclerView instance"); - } - } - - mRecyclerView = rv; - - if (scrollEventDistributor != null) { - scrollEventDistributor.add(mInternalUseOnScrollListener); - mScrollEventRegisteredToDistributor = true; - } else { - mRecyclerView.setOnScrollListener(mInternalUseOnScrollListener); - mScrollEventRegisteredToDistributor = false; - } - - mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener); - - mDisplayDensity = mRecyclerView.getResources().getDisplayMetrics().density; - mTouchSlop = ViewConfiguration.get(mRecyclerView.getContext()).getScaledTouchSlop(); - mScrollTouchSlop = (int) (mTouchSlop * SCROLL_TOUCH_SLOP_MULTIPLY + 0.5f); - - if (supportsEdgeEffect()) { - // edge effect is available on ICS or later - mEdgeEffectDecorator = new EdgeEffectDecorator(mRecyclerView); - mEdgeEffectDecorator.start(); - } - } - - /** - * 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() { - cancelDrag(); - - if (mEdgeEffectDecorator != null) { - mEdgeEffectDecorator.finish(); - mEdgeEffectDecorator = null; - } - - if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) { - mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); - } - mInternalUseOnItemTouchListener = null; - - if (mRecyclerView != null && mInternalUseOnScrollListener != null && mScrollEventRegisteredToDistributor) { - mRecyclerView.setOnScrollListener(null); - } - mInternalUseOnScrollListener = null; - - if (mScrollOnDraggingProcess != null) { - mScrollOnDraggingProcess.release(); - mScrollOnDraggingProcess = null; - } - mAdapter = null; - mRecyclerView = null; - mSwapTargetTranslationInterpolator = null; - mScrollEventRegisteredToDistributor = false; - } - - /** - * Indicates whether currently performing item dragging. - * - * @return True if currently performing item dragging - */ - public boolean isDragging() { - return (mDraggingItem != null) && (mDeferredCancelProcess == null); - } - - /** - * Sets 9-patch image which is used for the actively dragging item - * - * @param drawable The 9-patch drawable - */ - public void setDraggingItemShadowDrawable(NinePatchDrawable drawable) { - mShadowDrawable = drawable; - } - - /** - * Sets the interpolator which is used for determining the position of the swapping item. - * - * @param interpolator Interpolator to set or null to clear - */ - public void setSwapTargetTranslationInterpolator(Interpolator interpolator) { - mSwapTargetTranslationInterpolator = interpolator; - } - - /** - * Gets the interpolator which ise used for determining the position of the swapping item. - * - * @return Interpolator which is used for determining the position of the swapping item - */ - public Interpolator setSwapTargetTranslationInterpolator() { - return mSwapTargetTranslationInterpolator; - } - - /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onInterceptTouchEvent() action = " + action); - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - handleActionUpOrCancel(rv, e); - break; - - case MotionEvent.ACTION_DOWN: - if (!isDragging()) { - handleActionDown(rv, e); - } - break; - - case MotionEvent.ACTION_MOVE: - if (isDragging()) { - // NOTE: The first ACTION_MOVE event will come here. (maybe a bug of RecyclerView?) - handleActionMoveWhileDragging(rv, e); - return true; - } else { - if (handleActionMoveWhileNotDragging(rv, e)) { - return true; - } - } - } - - return false; - } - - /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) { - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onTouchEvent() action = " + action); - } - - if (!isDragging()) { - // Log.w(TAG, "onTouchEvent() - unexpected state"); - return; - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - handleActionUpOrCancel(rv, e); - break; - - case MotionEvent.ACTION_MOVE: - handleActionMoveWhileDragging(rv, e); - break; - - } - } - - /*package*/ void onScrolled(RecyclerView recyclerView, int dx, int dy) { - if (LOCAL_LOGV) { - Log.v(TAG, "onScrolled(dx = " + dx + ", dy = " + dy + ")"); - } - - if (mInScrollByMethod) { - mActualScrollByAmount = dy; - } - } - - /*package*/ void onScrollStateChanged(RecyclerView recyclerView, int newState) { - if (LOCAL_LOGV) { - Log.v(TAG, "onScrollStateChanged(newState = " + newState + ")"); - } - } - - private boolean handleActionDown(RecyclerView rv, MotionEvent e) { - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); - - if (!checkTouchedItemState(rv, holder)) { - return false; - } - - mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); - mInitialTouchItemId = holder.getItemId(); - - return true; - } - - @SuppressWarnings("unchecked") - private void startDragging(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, ItemDraggableRange range) { - safeEndAnimation(rv, holder); - - mDraggingItem = holder; - - // XXX if setIsRecyclable() is used, another view holder objects will be created - // which has the same ID with currently dragging item... Not works as expected. - - // mDraggingItem.setIsRecyclable(false); - - mDraggingItemId = mDraggingItem.getItemId(); - mDraggableRange = range; - - final View itemView = mDraggingItem.itemView; - - mOrigOverScrollMode = ViewCompat.getOverScrollMode(rv); - ViewCompat.setOverScrollMode(rv, ViewCompat.OVER_SCROLL_NEVER); - - mLastTouchY = (int) (e.getY() + 0.5f); - - // disable auto scrolling until user moves the item - mDragStartTouchY = mDragMinTouchY = mDragMaxTouchY = mLastTouchY; - mScrollDirMask = SCROLL_DIR_NONE; - - // calculate the view-local offset from the touched point - mGrabbedPositionY = mLastTouchY - itemView.getTop(); - - mGrabbedItemHeight = itemView.getHeight(); - CustomRecyclerViewUtils.getLayoutMargins(itemView, mDraggingItemMargins); - - mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); - - startScrollOnDraggingProcess(); - - // raise onDragItemStarted() event - mAdapter.onDragItemStarted(mDraggingItem, mDraggableRange); - - // setup decorators - mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getPosition()); - - mDraggingItemDecorator = new DraggingItemDecorator(mRecyclerView, mDraggingItem, mDraggableRange); - mDraggingItemDecorator.setShadowDrawable(mShadowDrawable); - mDraggingItemDecorator.start(e, mGrabbedPositionY); - - if (supportsViewTranslation()) { - mSwapTargetItemOperator = new SwapTargetItemOperator(mRecyclerView, mDraggingItem, mDraggableRange); - mSwapTargetItemOperator.setSwapTargetTranslationInterpolator(mSwapTargetTranslationInterpolator); - mSwapTargetItemOperator.start(); - mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY()); - } - - if (mEdgeEffectDecorator != null) { - mEdgeEffectDecorator.reorderToTop(); - } - } - - /** - * Cancel dragging. - */ - public void cancelDrag() { - cancelDrag(false); - } - - private void cancelDrag(boolean immediately) { - if (immediately) { - finishDragging(false); - } else { - if (mDraggingItem != null) { - if (mDeferredCancelProcess == null) { - mDeferredCancelProcess = new Runnable() { - @Override - public void run() { - if (mDeferredCancelProcess == this) { - mDeferredCancelProcess = null; - finishDragging(false); - } - } - }; - mRecyclerView.post(mDeferredCancelProcess); - } - } - } - } - - private void finishDragging(boolean result) { - final RecyclerView.ViewHolder draggedItem = mDraggingItem; - - // cancel deferred request - if (mDeferredCancelProcess != null) { - mRecyclerView.removeCallbacks(mDeferredCancelProcess); - mDeferredCancelProcess = null; - } - - // NOTE: setOverScrollMode() have to be called before calling removeItemDecoration() - if (mRecyclerView != null && mDraggingItem != null) { - ViewCompat.setOverScrollMode(mRecyclerView, mOrigOverScrollMode); - } - - if (mDraggingItemDecorator != null) { - mDraggingItemDecorator.setReturnToDefaultPositionAnimationDuration(mItemSettleBackIntoPlaceAnimationDuration); - mDraggingItemDecorator.setReturnToDefaultPositionAnimationInterpolator(mItemSettleBackIntoPlaceAnimationInterpolator); - mDraggingItemDecorator.finish(true); - } - - if (mSwapTargetItemOperator != null) { - mSwapTargetItemOperator.setReturnToDefaultPositionAnimationDuration(mItemSettleBackIntoPlaceAnimationDuration); - mDraggingItemDecorator.setReturnToDefaultPositionAnimationInterpolator(mItemSettleBackIntoPlaceAnimationInterpolator); - mSwapTargetItemOperator.finish(true); - } - - if (mEdgeEffectDecorator != null) { - mEdgeEffectDecorator.releaseBothGlows(); - } - - stopScrollOnDraggingProcess(); - - if (mRecyclerView != null && mRecyclerView.getParent() != null) { - mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); - } - - mDraggableRange = null; - mDraggingItemDecorator = null; - mSwapTargetItemOperator = null; - mDraggingItem = null; - mDraggingItemId = RecyclerView.NO_ID; - - mLastTouchY = 0; - mDragStartTouchY = 0; - mDragMinTouchY = 0; - mDragMaxTouchY = 0; - mGrabbedPositionY = 0; - mGrabbedItemHeight = 0; - - // raise onDragItemFinished() event - if ((mAdapter != null) && (draggedItem != null)) { - mAdapter.onDragItemFinished(draggedItem, result); - } - -// if (draggedItem != null) { -// draggedItem.setIsRecyclable(true); -// } - } - - private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) { - final boolean result = (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP); - - mInitialTouchY = 0; - mLastTouchY = 0; - mDragStartTouchY = 0; - mDragMinTouchY = 0; - mDragMaxTouchY = 0; - mInitialTouchItemId = RecyclerView.NO_ID; - - if (isDragging()) { - if (LOCAL_LOGD) { - Log.d(TAG, "dragging finished --- result = " + result); - } - - finishDragging(result); - } - - return true; - } - - private boolean handleActionMoveWhileNotDragging(RecyclerView rv, MotionEvent e) { - final int touchX = (int) (e.getX() + 0.5f); - final int touchY = (int) (e.getY() + 0.5f); - - mLastTouchY = touchY; - - if (mInitialTouchItemId == RecyclerView.NO_ID) { - return false; - } - - if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) { - return false; - } - - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); - - if (!checkTouchedItemState(rv, holder)) { - mInitialTouchItemId = RecyclerView.NO_ID; - return false; - } - - if (holder.getItemId() != mInitialTouchItemId) { - mInitialTouchItemId = RecyclerView.NO_ID; - 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); - - if (!mAdapter.canStartDrag(holder, viewX, viewY)) { - return false; - } - - ItemDraggableRange range = mAdapter.getItemDraggableRange(holder); - - if (range == null) { - range = new ItemDraggableRange(0, Math.max(0, mAdapter.getItemCount() - 1)); - } - - verifyItemDraggableRange(range, holder); - - - if (LOCAL_LOGD) { - Log.d(TAG, "dragging started"); - } - - startDragging(rv, e, holder, range); - - return true; - - } - - private void verifyItemDraggableRange(ItemDraggableRange range, RecyclerView.ViewHolder holder) { - final int start = 0; - final int end = Math.max(0, mAdapter.getItemCount() - 1); - - if (range.getStart() > range.getEnd()) { - throw new IllegalStateException("Invalid range specified --- start > range (range = " + range + ")"); - } - - if (range.getStart() < start) { - throw new IllegalStateException("Invalid range specified --- start < 0 (range = " + range + ")"); - } - - if (range.getEnd() > end) { - throw new IllegalStateException("Invalid range specified --- end >= count (range = " + range + ")"); - } - - if (!range.checkInRange(holder.getPosition())) { - throw new IllegalStateException( - "Invalid range specified --- does not contain drag target item" - + " (range = " + range + ", position = " + holder.getPosition() + ")"); - } - } - - private void handleActionMoveWhileDragging(RecyclerView rv, MotionEvent e) { - mLastTouchY = (int) (e.getY() + 0.5f); - mDragMinTouchY = Math.min(mDragMinTouchY, mLastTouchY); - mDragMaxTouchY = Math.max(mDragMaxTouchY, mLastTouchY); - - // update drag direction mask - updateDragDirectionMask(); - - // update decorators - mDraggingItemDecorator.update(e); - if (mSwapTargetItemOperator != null) { - mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY()); - } - - // check swapping - checkItemSwapping(rv); - } - - private void updateDragDirectionMask() { - if (((mDragStartTouchY - mDragMinTouchY) > mScrollTouchSlop) || - ((mDragMaxTouchY - mLastTouchY) > mScrollTouchSlop)) { - mScrollDirMask |= SCROLL_DIR_UP; - } - if (((mDragMaxTouchY - mDragStartTouchY) > mScrollTouchSlop) || - ((mLastTouchY - mDragMinTouchY) > mScrollTouchSlop)) { - mScrollDirMask |= SCROLL_DIR_DOWN; - } - } - - private void checkItemSwapping(RecyclerView rv) { - final RecyclerView.ViewHolder draggingItem = mDraggingItem; - - final int overlayItemTop = mLastTouchY - mGrabbedPositionY; - final RecyclerView.ViewHolder swapTargetHolder = - findSwapTargetItem(rv, draggingItem, mDraggingItemId, overlayItemTop, mDraggableRange); - - if ((swapTargetHolder != null) && (swapTargetHolder != mDraggingItem)) { - swapItems(rv, draggingItem, swapTargetHolder); - } - } - - /*package*/ void handleScrollOnDragging() { - final RecyclerView rv = mRecyclerView; - final int height = rv.getHeight(); - - if (height == 0) { - return; - } - - final float invHeight = (1.0f / height); - final float y = mLastTouchY * invHeight; - final float threshold = SCROLL_THRESHOLD; - final float invThreshold = (1.0f / threshold); - final float centerOffset = y - 0.5f; - final float absCenterOffset = Math.abs(centerOffset); - final float acceleration = Math.max(0.0f, threshold - (0.5f - absCenterOffset)) * invThreshold; - final int mask = mScrollDirMask; - - int scrollAmount = (int) Math.signum(centerOffset) * (int) (SCROLL_AMOUNT_COEFF * mDisplayDensity * acceleration + 0.5f); - int actualScrolledAmount = 0; - - final ItemDraggableRange range = mDraggableRange; - - final int firstVisibleChild = CustomRecyclerViewUtils.findFirstCompletelyVisibleItemPosition(mRecyclerView); - final int lastVisibleChild = CustomRecyclerViewUtils.findLastCompletelyVisibleItemPosition(mRecyclerView); - - boolean reachedToTopHardLimit = false; - boolean reachedToTopSoftLimit = false; - boolean reachedToBottomHardLimit = false; - boolean reachedToBottomSoftLimit = false; - - if (firstVisibleChild != RecyclerView.NO_POSITION) { - if (firstVisibleChild <= range.getStart()) { - reachedToTopSoftLimit = true; - } - if (firstVisibleChild <= (range.getStart() - 1)) { - reachedToTopHardLimit = true; - } - } - - if (lastVisibleChild != RecyclerView.NO_POSITION) { - if (lastVisibleChild >= range.getEnd()) { - reachedToBottomSoftLimit = true; - } - if (lastVisibleChild >= (range.getEnd() + 1)) { - reachedToBottomHardLimit = true; - } - } - - // apply mask - if (scrollAmount > 0) { - if ((mask & SCROLL_DIR_DOWN) == 0) { - scrollAmount = 0; - } - } else if (scrollAmount < 0) { - if ((mask & SCROLL_DIR_UP) == 0) { - scrollAmount = 0; - } - } - - if ((!reachedToTopHardLimit && (scrollAmount < 0)) || - (!reachedToBottomHardLimit && (scrollAmount > 0))) { - safeEndAnimations(rv); - actualScrolledAmount = scrollByYAndGetScrolledAmount(scrollAmount); - - if (scrollAmount < 0) { - mDraggingItemDecorator.setIsScrolling(!reachedToTopSoftLimit); - } else { - mDraggingItemDecorator.setIsScrolling(!reachedToBottomSoftLimit); - } - - mDraggingItemDecorator.refresh(); - if (mSwapTargetItemOperator != null) { - mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY()); - } - } else { - mDraggingItemDecorator.setIsScrolling(false); - } - - final boolean actualIsScrolling = (actualScrolledAmount != 0); - - - if (mEdgeEffectDecorator != null) { - 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 float nearEdgeOffset = (nearEdgePosition * invHeight) - 0.5f; - final float absNearEdgeOffset = Math.abs(nearEdgeOffset); - final float edgeEffectAcceleration = Math.max(0.0f, threshold - (0.5f - absNearEdgeOffset)) * invThreshold; - - if ((edgeEffectAcceleration >= EDGE_EFFECT_THRESHOLD) && (scrollAmount != 0) && !actualIsScrolling) { - // over scrolled - final float distance = edgeEffectAcceleration * 0.02f; - - if (scrollAmount < 0) { - // upward - mEdgeEffectDecorator.pullTopGlow(distance); - } else { - // downward - mEdgeEffectDecorator.pullBottom(distance); - } - } else { - mEdgeEffectDecorator.releaseBothGlows(); - } - } - - checkItemSwapping(rv); - } - - private int scrollByYAndGetScrolledAmount(int ry) { - // NOTE: mActualScrollByAmount --- Hackish! To detect over scrolling. - - mActualScrollByAmount = 0; - mInScrollByMethod = true; - mRecyclerView.scrollBy(0, ry); - mInScrollByMethod = false; - - return mActualScrollByAmount; - } - - /*package*/ RecyclerView getRecyclerView() { - return mRecyclerView; - } - - private void startScrollOnDraggingProcess() { - mScrollOnDraggingProcess.start(); - } - - private void stopScrollOnDraggingProcess() { - if (mScrollOnDraggingProcess != null) { - mScrollOnDraggingProcess.stop(); - } - } - - 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 diffPosition = Math.abs(fromPosition - toPosition); - boolean performSwapping = false; - - if (fromPosition == RecyclerView.NO_POSITION || toPosition == RecyclerView.NO_POSITION) { - return; - } - - final long actualDraggingItemId = rv.getAdapter().getItemId(fromPosition); - if (actualDraggingItemId != mDraggingItemId) { - if (LOCAL_LOGV) { - Log.v(TAG, "RecyclerView state has not been synched to data yet"); - } - return; - } - - //noinspection StatementWithEmptyBody - if (diffPosition == 0) { - } else if (diffPosition == 1) { - final View v1 = draggingItem.itemView; - final View v2 = swapTargetHolder.itemView; - final Rect m1 = mDraggingItemMargins; - //noinspection UnnecessaryLocalVariable - final Rect m2 = swapTargetMargins; - - final int top = Math.min(v1.getTop() - m1.top, v2.getTop() - m2.top); - final int bottom = Math.max(v1.getBottom() + m1.bottom, v2.getBottom() + m2.bottom); - - final float midPointOfTheItems = top + ((bottom - top) * 0.5f); - final float midPointOfTheOverlaidItem = (mLastTouchY - mGrabbedPositionY) + (mGrabbedItemHeight * 0.5f); - - if (toPosition < fromPosition) { - if (midPointOfTheOverlaidItem < midPointOfTheItems) { - // swap (up direction) - performSwapping = true; - } - } else { // if (toPosition > fromPosition) - if (midPointOfTheOverlaidItem > midPointOfTheItems) { - // swap (down direction) - performSwapping = true; - } - } - } else { // diffPosition > 1 - performSwapping = true; - } - - if (performSwapping) { - if (LOCAL_LOGD) { - Log.d(TAG, "item swap (from: " + fromPosition + ", to: " + toPosition + ")"); - } - - RecyclerView.ViewHolder firstVisibleTopItem = null; - - if (rv.getChildCount() > 0) { - View child = rv.getChildAt(0); - if (child != null) { - firstVisibleTopItem = rv.getChildViewHolder(child); - } - } - final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getPosition() : RecyclerView.NO_POSITION; - - - mAdapter.moveItem(fromPosition, toPosition); - - if (fromPosition == prevTopItemPosition) { - //noinspection UnnecessaryLocalVariable - final Rect margins = swapTargetMargins; - final int curTopItemHeight = swapTargetHolder.itemView.getHeight() + margins.top + margins.bottom; - scrollByYAndGetScrolledAmount(-curTopItemHeight); - } else if (toPosition == prevTopItemPosition) { - final Rect margins = mDraggingItemMargins; - final int curTopItemHeight = mGrabbedItemHeight + margins.top + margins.bottom; - scrollByYAndGetScrolledAmount(-curTopItemHeight); - } - - safeEndAnimations(rv); - } - } - - private static DraggableItemWrapperAdapter getDraggableItemWrapperAdapter(RecyclerView rv) { - return WrapperAdapterUtils.findWrappedAdapter(rv.getAdapter(), DraggableItemWrapperAdapter.class); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean checkTouchedItemState(RecyclerView rv, RecyclerView.ViewHolder holder) { - if (!(holder instanceof DraggableItemViewHolder)) { - return false; - } - - final int itemPosition = holder.getPosition(); - final RecyclerView.Adapter adapter = rv.getAdapter(); - - // verify the touched item is valid state - if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) { - return false; - } - - //noinspection RedundantIfStatement - if (holder.getItemId() != adapter.getItemId(itemPosition)) { - return false; - } - - return true; - } - - private static boolean supportsEdgeEffect() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; - } - - private static boolean supportsViewTranslation() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; - } - - private static void safeEndAnimation(RecyclerView rv, RecyclerView.ViewHolder holder) { - final RecyclerView.ItemAnimator itemAnimator = (rv != null) ? rv.getItemAnimator() : null; - if (itemAnimator != null) { - itemAnimator.endAnimation(holder); - } - } - - private static void safeEndAnimations(RecyclerView rv) { - final RecyclerView.ItemAnimator itemAnimator = (rv != null) ? rv.getItemAnimator() : null; - if (itemAnimator != null) { - itemAnimator.endAnimations(); - } - } - - /*package*/ - static RecyclerView.ViewHolder findSwapTargetItem( - RecyclerView rv, RecyclerView.ViewHolder draggingItem, - long draggingItemId, int overlayItemTop, ItemDraggableRange range) { - final int draggingItemPosition = draggingItem.getPosition(); - final int draggingViewTop = draggingItem.itemView.getTop(); - RecyclerView.ViewHolder swapTargetHolder = null; - - // determine the swap target view - if (draggingItemPosition != RecyclerView.NO_POSITION && - draggingItem.getItemId() == draggingItemId) { - if (overlayItemTop < draggingViewTop) { - if (draggingItemPosition > 0) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition - 1); - } - } else if (overlayItemTop > draggingViewTop) { - if (draggingItemPosition < (rv.getAdapter().getItemCount() - 1)) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition + 1); - } - } - } - - // check range - if (swapTargetHolder != null && range != null) { - if (!range.checkInRange(swapTargetHolder.getPosition())) { - swapTargetHolder = null; - } - } - - return swapTargetHolder; - } - - /** - * Sets the duration of "settle back into place" animation. - * - * @param duration Specify the animation duration in milliseconds - */ - public void setItemSettleBackIntoPlaceAnimationDuration(int duration) { - mItemSettleBackIntoPlaceAnimationDuration = duration; - } - - /** - * Gets the duration of "settle back into place" animation. - * - * @return The duration of "settle back into place" animation in milliseconds - */ - public int getItemSettleBackIntoPlaceAnimationDuration() { - return mItemSettleBackIntoPlaceAnimationDuration; - } - - /** - * Sets the interpolator which is used for "settle back into place" animation. - * - * @param interpolator Interpolator to set or null to clear - */ - public void setItemSettleBackIntoPlaceAnimationInterpolator(Interpolator interpolator) { - mItemSettleBackIntoPlaceAnimationInterpolator = interpolator; - } - - /** - * Gets the interpolator which ise used for "settle back into place" animation. - * - * @return Interpolator which is used for "settle back into place" animation - */ - public Interpolator getItemSettleBackIntoPlaceAnimationInterpolator() { - return mItemSettleBackIntoPlaceAnimationInterpolator; - } - - private static class ScrollOnDraggingProcessRunnable implements Runnable { - private final WeakReference mHolderRef; - private boolean mStarted; - - public ScrollOnDraggingProcessRunnable(RecyclerViewDragDropManager holder) { - mHolderRef = new WeakReference<>(holder); - } - - public void start() { - if (mStarted) { - return; - } - - final RecyclerViewDragDropManager holder = mHolderRef.get(); - - if (holder == null) { - return; - } - - final RecyclerView rv = holder.getRecyclerView(); - - if (rv == null) { - return; - } - - ViewCompat.postOnAnimation(rv, this); - - mStarted = true; - } - - public void stop() { - if (!mStarted) { - return; - } - - mStarted = false; - } - - public void release() { - mHolderRef.clear(); - mStarted = false; - } - - @Override - public void run() { - final RecyclerViewDragDropManager holder = mHolderRef.get(); - - if (holder == null) { - return; - } - - if (!mStarted) { - return; - } - - // call scrolling process - holder.handleScrollOnDragging(); - - // re-schedule the process - final RecyclerView rv = holder.getRecyclerView(); - - if (rv != null && mStarted) { - ViewCompat.postOnAnimation(rv, this); - } else { - mStarted = false; - } - } - } -} 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 deleted file mode 100644 index 66e58c69121..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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.draggable; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.animation.Interpolator; - -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; - -class SwapTargetItemOperator extends BaseDraggableItemDecorator { - @SuppressWarnings("unused") - private static final String TAG = "SwapTargetItemOperator"; - - private RecyclerView.ViewHolder mSwapTargetItem; - private Interpolator mSwapTargetTranslationInterpolator; - private int mTranslationY; - private int mDraggingItemHeight; - private Rect mSwapTargetDecorationOffsets = new Rect(); - private Rect mSwapTargetItemMargins = new Rect(); - private Rect mDraggingItemMargins = new Rect(); - private Rect mDraggingItemDecorationOffsets = new Rect(); - private boolean mStarted; - private float mReqTranslationPhase; - private float mCurTranslationPhase; - private long mDraggingItemId; - private ItemDraggableRange mRange; - - public SwapTargetItemOperator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem, ItemDraggableRange range) { - super(recyclerView, draggingItem); - - mDraggingItemId = mDraggingItem.getItemId(); - mRange = range; - - CustomRecyclerViewUtils.getLayoutMargins(mDraggingItem.itemView, mDraggingItemMargins); - CustomRecyclerViewUtils.getDecorationOffsets( - mRecyclerView.getLayoutManager(), mDraggingItem.itemView, mDraggingItemDecorationOffsets); - } - - public void setSwapTargetTranslationInterpolator(Interpolator interpolator) { - mSwapTargetTranslationInterpolator = interpolator; - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - final RecyclerView.ViewHolder draggingItem = mDraggingItem; - - if (draggingItem == null || draggingItem.getItemId() != mDraggingItemId) { - return; - } - - final RecyclerView.ViewHolder swapTargetItem = - RecyclerViewDragDropManager.findSwapTargetItem( - mRecyclerView, draggingItem, mDraggingItemId, mTranslationY, mRange); - - // reset Y-translation if the swap target has changed - if ((mSwapTargetItem != swapTargetItem) && (mSwapTargetItem != null)) { - setItemTranslationY(mRecyclerView, mSwapTargetItem, 0.0f); - } - - if (swapTargetItem != null) { - mReqTranslationPhase = calculateTranslationPhase(draggingItem, swapTargetItem); - - if (mSwapTargetItem != swapTargetItem) { - mCurTranslationPhase = mReqTranslationPhase; - } else { - // interpolate to make it moves smoothly - mCurTranslationPhase = calculateCurrentTranslationPhase(mCurTranslationPhase, mReqTranslationPhase); - } - - updateSwapTargetTranslation(draggingItem, swapTargetItem, mCurTranslationPhase); - } - - mSwapTargetItem = swapTargetItem; - } - - private float calculateTranslationPhase(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem) { - final View swapItemView = swapTargetItem.itemView; - - final int pos1 = draggingItem.getPosition(); - final int pos2 = swapTargetItem.getPosition(); - - CustomRecyclerViewUtils.getDecorationOffsets( - mRecyclerView.getLayoutManager(), swapItemView, mSwapTargetDecorationOffsets); - CustomRecyclerViewUtils.getLayoutMargins(swapItemView, mSwapTargetItemMargins); - - final Rect m2 = mSwapTargetItemMargins; - final Rect d2 = mSwapTargetDecorationOffsets; - final int h2 = swapItemView.getHeight() + m2.top + m2.bottom + d2.top + d2.bottom; - - final float offsetPx = draggingItem.itemView.getTop() - mTranslationY; // == -(ViewCompat.getTranslationY(draggingItem.itemView) - final float phase = (h2 != 0) ? (offsetPx / h2) : 0.0f; - - float translationPhase; - - if (pos1 > pos2) { - // dragging item moving to upward - translationPhase = phase; - } else { - // dragging item moving to downward - translationPhase = 1.0f + phase; - } - - return Math.min(Math.max(translationPhase, 0.0f), 1.0f); - } - - private static float calculateCurrentTranslationPhase(float cur, float req) { - final float A = 0.3f; - final float B = 0.01f; - final float tmp = (cur * (1.0f - A)) + (req * A); - - return (Math.abs(tmp - req) < B) ? req : tmp; - } - - 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 Rect m1 = mDraggingItemMargins; - final Rect d1 = mDraggingItemDecorationOffsets; - final int h1 = mDraggingItemHeight + m1.top + m1.bottom + d1.top + d1.bottom; - - if (mSwapTargetTranslationInterpolator != null) { - translationPhase = mSwapTargetTranslationInterpolator.getInterpolation(translationPhase); - } - - if (pos1 > pos2) { - // dragging item moving to upward - ViewCompat.setTranslationY(swapItemView, translationPhase * h1); - } else { - // dragging item moving to downward - ViewCompat.setTranslationY(swapItemView, (translationPhase - 1.0f) * h1); - } - } - - public void start() { - if (mStarted) { - return; - } - - mDraggingItemHeight = mDraggingItem.itemView.getHeight(); - - mRecyclerView.addItemDecoration(this, 0); - - mStarted = true; - } - - public void finish(boolean animate) { - if (mStarted) { - mRecyclerView.removeItemDecoration(this); - } - - final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); - if (itemAnimator != null) { - itemAnimator.endAnimations(); - } - mRecyclerView.stopScroll(); - - if (mSwapTargetItem != null) { - // return to default position - updateSwapTargetTranslation(mDraggingItem, mSwapTargetItem, mCurTranslationPhase); - moveToDefaultPosition(mSwapTargetItem.itemView, animate); - mSwapTargetItem = null; - } - - mRange = null; - mDraggingItem = null; - mTranslationY = 0; - mDraggingItemHeight = 0; - mCurTranslationPhase = 0.0f; - mReqTranslationPhase = 0.0f; - mStarted = false; - } - - public void update(int translationY) { - mTranslationY = translationY; - } -} \ No newline at end of file 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 deleted file mode 100644 index a7f8d022a72..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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.event; - -import android.support.v7.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -public abstract class BaseRecyclerViewEventDistributor { - protected boolean mReleased; - protected RecyclerView mRecyclerView; - protected List mListeners; - protected boolean mPerformingClearMethod; - - public BaseRecyclerViewEventDistributor() { - } - - /** - * Gets attached {@link android.support.v7.widget.RecyclerView} - * - * @return The {@link android.support.v7.widget.RecyclerView} instance - */ - public RecyclerView getRecyclerView() { - return mRecyclerView; - } - - /** - * Release all references. - * This method should be called to avoid memory leaks. - */ - public void release() { - if (mReleased) { - return; - } - mReleased = true; - clear(true); - onRelease(); - } - - /** - * Indicates whether this distributor has been released - * - * @return True for this distributor has been released, otherwise false - */ - public boolean isReleased() { - return mReleased; - } - - /** - * Attaches {@link android.support.v7.widget.RecyclerView} instance. - * - * @param rv The {@link android.support.v7.widget.RecyclerView} instance - */ - public void attachRecyclerView(RecyclerView rv) { - final String METHOD_NAME = "attachRecyclerView()"; - - if (rv == null) { - throw new IllegalArgumentException("RecyclerView cannot be null"); - } - - verifyIsNotReleased(METHOD_NAME); - verifyIsNotPerformingClearMethod(METHOD_NAME); - - onRecyclerViewAttached(rv); - } - - /** - * Add a {@link T} listener to the chain. - * - * @param listener The {@link T} instance - * - * @return True if the listener object successfully added, otherwise false. Also returns true if have already been added. - */ - public boolean add(T listener) { - return add(listener, -1); - } - - /** - * Add a {@link T} listener to the chain at the specified position. - * - * @param listener The {@link T} instance - * @param index Position in the listener chain to insert this listener at. (< 0: tail of the chain) - * - * @return True if the listener object successfully added, otherwise false. Also returns true if have already been added. - */ - public boolean add(T listener, int index) { - final String METHOD_NAME = "add()"; - - if (listener == null) { - throw new IllegalArgumentException("can not specify null for the listener"); - } - - verifyIsNotReleased(METHOD_NAME); - verifyIsNotPerformingClearMethod(METHOD_NAME); - - if (mListeners == null) { - mListeners = new ArrayList<>(); - } - - if (!mListeners.contains(listener)) { - if (index < 0) { - // append to the tail of the list - mListeners.add(listener); - } else { - // insert to the specified position - mListeners.add(index, listener); - } - - // raise onAddedToEventDistributor() event - if (listener instanceof RecyclerViewEventDistributorListener) { - ((RecyclerViewEventDistributorListener) listener).onAddedToEventDistributor(this); - } - } - - return true; - } - - /** - * Remove a {@link T} listener from the chain. - * - * @param listener Listener to remove - * - * @return True for successfully removed the listener object, otherwise false - */ - public boolean remove(T listener) { - final String METHOD_NAME = "remove()"; - - if (listener == null) { - throw new IllegalArgumentException("can not specify null for the listener"); - } - - verifyIsNotPerformingClearMethod(METHOD_NAME); - verifyIsNotReleased(METHOD_NAME); - - if (mListeners == null) { - return false; - } - - final boolean removed = mListeners.remove(listener); - - if (removed) { - // raise onRemovedFromEventDistributor() event - if (listener instanceof RecyclerViewEventDistributorListener) { - ((RecyclerViewEventDistributorListener) listener).onRemovedFromEventDistributor(this); - } - } - - return removed; - } - - /** - * Remove all listeners from the chain. - */ - public void clear() { - clear(false); - } - - protected void clear(boolean calledFromRelease) { - final String METHOD_NAME = "clear()"; - - if (!calledFromRelease) { - verifyIsNotReleased(METHOD_NAME); - } - verifyIsNotPerformingClearMethod(METHOD_NAME); - - if (mListeners == null) { - return; - } - - try { - mPerformingClearMethod = true; - - final int n = mListeners.size(); - - for (int i = n - 1; i >= 0; i--) { - final T listener = mListeners.remove(i); - - // raise onRemovedFromEventDistributor() event - if (listener instanceof RecyclerViewEventDistributorListener) { - ((RecyclerViewEventDistributorListener) listener).onRemovedFromEventDistributor(this); - } - } - } finally { - mPerformingClearMethod = false; - } - } - - /** - * Gets the number of underlying listener objects. - * - * @return Number of underlying listener objects in the chain. - */ - public int size() { - if (mListeners != null) { - return mListeners.size(); - } else { - return mListeners.size(); - } - } - - /** - * Gets whether the specified listener object is contained in the chain. - * - * @param listener Listener to check - * - * @return True for the listener contains in the chain, otherwise false - */ - public boolean contains(T listener) { - if (mListeners != null) { - return mListeners.contains(listener); - } else { - return false; - } - } - - protected void onRelease() { - mRecyclerView = null; - mListeners = null; - mPerformingClearMethod = false; - } - - protected void onRecyclerViewAttached(RecyclerView rv) { - mRecyclerView = rv; - } - - protected void verifyIsNotPerformingClearMethod(String methodName) { - if (mPerformingClearMethod) { - throw new IllegalStateException(methodName + " can not be called while performing the clear() method"); - } - } - - protected void verifyIsNotReleased(String methodName) { - if (mReleased) { - throw new IllegalStateException(methodName + " can not be called after release() method called"); - } - } -} 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 deleted file mode 100644 index 437b7dc0377..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.event; - -public interface RecyclerViewEventDistributorListener { - void onAddedToEventDistributor(BaseRecyclerViewEventDistributor distributor); - void onRemovedFromEventDistributor(BaseRecyclerViewEventDistributor distributor); -} 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 deleted file mode 100644 index fb9ff5d79d9..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.event; - -import android.support.v7.widget.RecyclerView; - -import java.lang.ref.WeakReference; - -public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventDistributor { - - private InternalOnScrollListener mInternalOnScrollListener; - - public RecyclerViewOnScrollEventDistributor() { - super(); - - mInternalOnScrollListener = new InternalOnScrollListener(this); - } - - @Override - protected void onRecyclerViewAttached(RecyclerView rv) { - super.onRecyclerViewAttached(rv); - - rv.setOnScrollListener(mInternalOnScrollListener); - } - - @Override - protected void onRelease() { - super.onRelease(); - - if (mInternalOnScrollListener != null) { - mInternalOnScrollListener.release(); - mInternalOnScrollListener = null; - } - } - - /*package*/ void handleOnScrollStateChanged(RecyclerView recyclerView, int newState) { - if (mListeners == null) { - return; - } - - for (RecyclerView.OnScrollListener listener : mListeners) { - listener.onScrollStateChanged(recyclerView, newState); - } - } - - /*package*/ void handleOnScrolled(RecyclerView recyclerView, int dx, int dy) { - if (mListeners == null) { - return; - } - - for (RecyclerView.OnScrollListener listener : mListeners) { - listener.onScrolled(recyclerView, dx, dy); - } - } - - private static class InternalOnScrollListener extends RecyclerView.OnScrollListener { - private WeakReference mRefDistributor; - - public InternalOnScrollListener(RecyclerViewOnScrollEventDistributor distributor) { - super(); - mRefDistributor = new WeakReference<>(distributor); - } - - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - final RecyclerViewOnScrollEventDistributor distributor = mRefDistributor.get(); - if (distributor != null) { - distributor.handleOnScrollStateChanged(recyclerView, newState); - } - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - final RecyclerViewOnScrollEventDistributor holder = mRefDistributor.get(); - if (holder != null) { - holder.handleOnScrolled(recyclerView, dx, dy); - } - } - - public void release() { - mRefDistributor.clear(); - } - } -} 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 deleted file mode 100644 index d88870637f3..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.event; - -import android.support.v7.widget.RecyclerView; - -import java.lang.ref.WeakReference; - -public class RecyclerViewRecyclerEventDistributor extends BaseRecyclerViewEventDistributor { - - private InternalRecyclerListener mInternalRecyclerListener; - - public RecyclerViewRecyclerEventDistributor() { - super(); - - mInternalRecyclerListener = new InternalRecyclerListener(this); - } - - @Override - protected void onRecyclerViewAttached(RecyclerView rv) { - super.onRecyclerViewAttached(rv); - - rv.setRecyclerListener(mInternalRecyclerListener); - } - - @Override - protected void onRelease() { - super.onRelease(); - - if (mInternalRecyclerListener != null) { - mInternalRecyclerListener.release(); - mInternalRecyclerListener = null; - } - } - - /*package*/ void handleOnViewRecycled(RecyclerView.ViewHolder holder) { - if (mListeners == null) { - return; - } - - for (RecyclerView.RecyclerListener listener : mListeners) { - listener.onViewRecycled(holder); - } - } - - private static class InternalRecyclerListener implements RecyclerView.RecyclerListener { - private WeakReference mRefDistributor; - - public InternalRecyclerListener(RecyclerViewRecyclerEventDistributor distributor) { - super(); - mRefDistributor = new WeakReference<>(distributor); - } - - @Override - public void onViewRecycled(RecyclerView.ViewHolder holder) { - final RecyclerViewRecyclerEventDistributor distributor = mRefDistributor.get(); - - if (distributor != null) { - distributor.handleOnViewRecycled(holder); - } - } - - public void release() { - mRefDistributor.clear(); - } - } -} 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 deleted file mode 100644 index e1db29130c5..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; - -class ExpandableAdapterHelper { - public static final long NO_EXPANDABLE_POSITION = 0xffffffffffffffffl; - - private static final long LOWER_32BIT_MASK = 0x00000000ffffffffl; - private static final long LOWER_31BIT_MASK = 0x000000007fffffffl; - - /*package*/ static final int VIEW_TYPE_FLAG_IS_GROUP = 0x80000000; - - public static long getPackedPositionForChild(int groupPosition, int childPosition) { - return ((long) childPosition << 32) | (groupPosition & LOWER_32BIT_MASK); - } - - public static long getPackedPositionForGroup(int groupPosition) { - return ((long) RecyclerView.NO_POSITION << 32) | (groupPosition & LOWER_32BIT_MASK); - } - - public static int getPackedPositionChild(long packedPosition) { - return (int) (packedPosition >>> 32); - } - - public static int getPackedPositionGroup(long packedPosition) { - return (int) (packedPosition & LOWER_32BIT_MASK); - } - - public static long getCombinedChildId(long groupId, long childId) { - return ((groupId & LOWER_31BIT_MASK) << 32) | (childId & LOWER_32BIT_MASK); - } - - public static long getCombinedGroupId(long groupId) { - //noinspection PointlessBitwiseExpression - return ((groupId & LOWER_31BIT_MASK) << 32) | (RecyclerView.NO_ID & LOWER_32BIT_MASK); - } - - public static boolean isGroupViewType(int rawViewType) { - return ((rawViewType & VIEW_TYPE_FLAG_IS_GROUP) != 0); - } - - public static int getGroupViewType(int rawViewType) { - return (rawViewType & (~VIEW_TYPE_FLAG_IS_GROUP)); - } - - public static int getChildViewType(int rawViewType) { - return (rawViewType & (~VIEW_TYPE_FLAG_IS_GROUP)); - } - - private ExpandableAdapterHelper() { - } -} 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 deleted file mode 100644 index 1e5d5f368c3..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; - -import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; - -public interface ExpandableDraggableItemAdapter { - /** - * Called when user is attempt to drag the group item. - * - * @param holder The group ViewHolder which is associated to item user is attempt to start dragging. - * @param groupPosition Group position. - * @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 onCheckGroupCanStartDrag(GVH holder, int groupPosition, int x, int y); - - /** - * Called when user is attempt to drag the child item. - * - * @param holder The child ViewHolder which is associated to item user is attempt to start dragging. - * @param groupPosition Group position. - * @param childPosition Child position. - * @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 onCheckChildCanStartDrag(CVH holder, int groupPosition, int childPosition, int x, int y); - - /** - * Called after the {@link #onCheckGroupCanStartDrag(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 groupPosition Group position. - * - * @return null: no constraints (= new ItemDraggableRange(0, getGroupCount() - 1)), - * otherwise: the range specified item can be drag-sortable. - */ - ItemDraggableRange onGetGroupItemDraggableRange(GVH holder, int groupPosition); - - /** - * Called after the {@link #onCheckChildCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int, int)} method returned true. - * - * @param holder The ViewHolder which is associated to item user is attempt to start dragging. - * @param groupPosition Group position. - * @param childPosition Child position. - * - * @return null: no constraints (= new ItemDraggableRange(0, getGroupCount() - 1)), - * otherwise: the range specified item can be drag-sortable. - */ - ItemDraggableRange onGetChildItemDraggableRange(CVH holder, int groupPosition, int childPosition); - - /** - * Called when group item is moved. Should apply the move operation result to data set. - * - * @param fromGroupPosition Previous group position of the item. - * @param toGroupPosition New group position of the item. - */ - void onMoveGroupItem(int fromGroupPosition, int toGroupPosition); - - /** - * Called when child item is moved. Should apply the move operation result to data set. - * - * @param fromGroupPosition Previous group position of the item. - * @param fromChildPosition Previous child position of the item. - * @param toGroupPosition New group position of the item. - * @param toChildPosition New child position of the item. - */ - void onMoveChildItem(int fromGroupPosition, int fromChildPosition, int toGroupPosition, int toChildPosition); -} 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 deleted file mode 100644 index cc8cef577c8..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; - -public interface ExpandableItemAdapter { - - /** - * Gets the number of groups. - * - * @return the number of groups - */ - int getGroupCount(); - - /** - * Gets the number of children in a specified group. - * - * @param groupPosition the position of the group for which the children count should be returned - * - * @return the number of children - */ - int getChildCount(int groupPosition); - - /** - * Gets the ID for the group at the given position. This group ID must be unique across groups. - * - * The combined ID (see {@link RecyclerViewExpandableItemManager#getCombinedGroupId(long)}) - * must be unique across ALL items (groups and all children). - * - * @param groupPosition the position of the group for which the ID is wanted - * @return the ID associated with the group - */ - long getGroupId(int groupPosition); - - /** - * Gets the ID for the given child within the given group. - * - * This ID must be unique across all children within the group. - * The combined ID (see {@link RecyclerViewExpandableItemManager#getCombinedChildId(long, long)}) - * must be unique across ALL items (groups and all children). - * - * @param groupPosition the position of the group that contains the child - * @param childPosition the position of the child within the group for which the ID is wanted - - * @return the ID associated with the child - */ - long getChildId(int groupPosition, int childPosition); - - /** - * Gets the view type of the specified group. - * - * @param groupPosition the position of the group for which the view type is wanted - * - * @return integer value identifying the type of the view needed to represent the group item at position. Type codes need positive number but not be contiguous. - */ - int getGroupItemViewType(int groupPosition); - - /** - * Gets the view type of the specified child. - * - * @param groupPosition the position of the group that contains the child - * @param childPosition the position of the child within the group for which the view type is wanted - * - * @return integer value identifying the type of the view needed to represent the group item at position. Type codes need positive number but not be contiguous. - */ - int getChildItemViewType(int groupPosition, int childPosition); - - /** - * Called when RecyclerView needs a new {@link GVH} of the given type to represent a group item. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position - * @param viewType The view type of the new View - * - * @return A new group ViewHolder that holds a View of the given view type - */ - GVH onCreateGroupViewHolder(ViewGroup parent, int viewType); - - /** - * Called when RecyclerView needs a new {@link CVH} of the given type to represent a child item. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position - * @param viewType The view type of the new View - * - * @return A new child ViewHolder that holds a View of the given view type - */ - CVH onCreateChildViewHolder(ViewGroup parent, int viewType); - - /** - * Called by RecyclerView to display the group data at the specified position. - * This method should update the contents of the {@link android.support.v7.widget.RecyclerView.ViewHolder#itemView} - * to reflect the item at the given position. - * - * @param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set - * @param groupPosition The position of the group item within the adapter's data set - * @param viewType The view type code - */ - void onBindGroupViewHolder(GVH holder, int groupPosition, int viewType); - - /** - * Called by RecyclerView to display the child data at the specified position. - * This method should update the contents of the {@link android.support.v7.widget.RecyclerView.ViewHolder#itemView} - * to reflect the item at the given position. - * - * @param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set - * @param groupPosition The position of the group item within the adapter's data set - * @param childPosition The position of the child item within the group - * @param viewType The view type code - */ - void onBindChildViewHolder(CVH holder, int groupPosition, int childPosition, int viewType); - - /** - * Called when the user attempt to expand/collapse the group item. - * - * @param holder The ViewHolder which is associated to group item user is attempt to expand/collapse - * @param groupPosition Group position - * @param x Touched X position. Relative from the itemView's top-left - * @param y Touched Y position. Relative from the itemView's top-left - * @param expand true: expand, false: collapse - * - * @return Whether to perform expand/collapse operation. - */ - boolean onCheckCanExpandOrCollapseGroup(GVH holder, int groupPosition, int x, int y, boolean expand); -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java deleted file mode 100644 index 13803d4c6a8..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -/** - * Interface which provides required information for expanding item. - * - * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}. - */ -public interface ExpandableItemViewHolder { - /** - * Sets the state flags value for expanding item - * - * @param flags Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_GROUP} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_CHILD} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_EXPANDED} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_UPDATED} - */ - void setExpandStateFlags(int flags); - - /** - * Gets the state flags value for expanding item - * - * @return Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_GROUP} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_CHILD} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_EXPANDED} - * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_UPDATED} - */ - int getExpandStateFlags(); -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java deleted file mode 100644 index edd5e1db8a4..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java +++ /dev/null @@ -1,428 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; - -import java.util.Arrays; - -class ExpandablePositionTranslator { - private final static long FLAG_EXPANDED = 0x0000000080000000l; - private final static long LOWER_31BIT_MASK = 0x000000007fffffffl; - private final static long LOWER_32BIT_MASK = 0x00000000ffffffffl; - private final static long UPPER_32BIT_MASK = 0xffffffff00000000l; - - /* - * bit 64-32: offset (use for caching purpose) - * bit 31: expanded or not - * bit 30-0: child count - */ - private long[] mCachedGroupPosInfo; - - /* - * bit 31: reserved - * bit 30-0: group id - */ - private int[] mCachedGroupId; - private int mGroupCount; - private int mExpandedGroupCount; - private int mExpandedChildCount; - private int mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; - - public ExpandablePositionTranslator() { - } - - public void build(ExpandableItemAdapter adapter) { - final int groupCount = adapter.getGroupCount(); - - if (mCachedGroupPosInfo == null || mCachedGroupPosInfo.length < groupCount) { - mCachedGroupPosInfo = new long[groupCount]; - } - if (mCachedGroupId == null || mCachedGroupId.length < groupCount) { - mCachedGroupId = new int[groupCount]; - } - - final long[] info = mCachedGroupPosInfo; - final int[] ids = mCachedGroupId; - for (int i = 0; i < groupCount; 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 = groupCount; - mExpandedGroupCount = 0; - mExpandedChildCount = 0; - mEndOfCalculatedOffsetGroupPosition = Math.max(0, groupCount - 1); - } - - public void restoreExpandedGroupItems(int[] restoreGroupIds) { - if (restoreGroupIds == null || restoreGroupIds.length == 0) { - return; - } - - if (mCachedGroupPosInfo == null) { - return; - } - - // make ID + position packed array - final long [] idAndPos = new long[mGroupCount]; - - for (int i = 0; i < mGroupCount; i++) { - idAndPos[i] = ((long) mCachedGroupId[i] << 32) | i; - } - - // sort both arrays - Arrays.sort(restoreGroupIds); - Arrays.sort(idAndPos); - - // find matched items & apply - int index = 0; - //noinspection ForLoopReplaceableByForEach - for (int i = 0; i < restoreGroupIds.length; i++) { - final int id1 = restoreGroupIds[i]; - - for (int j = index; j < idAndPos.length; j++) { - final int id2 = (int) (idAndPos[j] >> 32); - - if (id2 < id1) { - index = j; - } else if (id2 == id1) { - // matched - final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK); - index = j + 1; - expandGroup(position); - break; - } else { // id2 > id1 - break; - } - } - } - } - - public int[] getSavedStateArray() { - int [] expandedGroups = new int[mExpandedGroupCount]; - - int index = 0; - for (int i = 0; i < mGroupCount; i++) { - final long t = mCachedGroupPosInfo[i]; - if ((t & FLAG_EXPANDED) != 0) { - expandedGroups[index] = mCachedGroupId[i]; - index += 1; - } - } - - if (index != mExpandedGroupCount) { - throw new IllegalStateException("may be a bug (index = " + index + ", mExpandedGroupCount = " + mExpandedGroupCount + ")"); - } - - return expandedGroups; - } - - public int getItemCount() { - return mGroupCount + mExpandedChildCount; - } - - public boolean isGroupExpanded(int groupPosition) { - return ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) != 0); - } - - public int getChildCount(int groupPosition) { - return (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK); - } - - public int getVisibleChildCount(int groupPosition) { - if (isGroupExpanded(groupPosition)) { - return getChildCount(groupPosition); - } else { - return 0; - } - } - - public boolean collapseGroup(int groupPosition) { - if ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) == 0) { - return false; - } - - final int childCount = (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK); - - mCachedGroupPosInfo[groupPosition] &= (~FLAG_EXPANDED); - mExpandedGroupCount -= 1; - - if (childCount > 0) { - mExpandedChildCount -= childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeRemoved() - return true; - } else { - return false; - } - } - - public boolean expandGroup(int groupPosition) { - if ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) != 0) { - return false; - } - - final int childCount = (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK); - - mCachedGroupPosInfo[groupPosition] |= FLAG_EXPANDED; - mExpandedGroupCount += 1; - - if (childCount > 0) { - mExpandedChildCount += childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeInserted() - return true; - } else { - return false; - } - } - - public void moveGroupItem(int fromGroupPosition, int toGroupPosition) { - if (fromGroupPosition == toGroupPosition) { - return; - } - - final long tmp1 = mCachedGroupPosInfo[fromGroupPosition]; - final int tmp2 = mCachedGroupId[fromGroupPosition]; - - if (toGroupPosition < fromGroupPosition) { - // shift to backward - for (int i = fromGroupPosition; i > toGroupPosition; i--) { - mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i - 1]; - mCachedGroupId[i] = mCachedGroupId[i - 1]; - } - } else { - // shift to forward - for (int i = fromGroupPosition; i < toGroupPosition; i++) { - mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1]; - mCachedGroupId[i] = mCachedGroupId[i + 1]; - } - } - - mCachedGroupPosInfo[toGroupPosition] = tmp1; - mCachedGroupId[toGroupPosition] = tmp2; - - final int minPosition = Math.min(fromGroupPosition, toGroupPosition); - - if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; - } else { - mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; - } - } - - public void moveChildItem(int fromGroupPosition, int fromChildPosition, int toGroupPosition, int toChildPosition) { - if (fromGroupPosition == toGroupPosition) { - return; - } - - final int fromChildCount = (int) (mCachedGroupPosInfo[fromGroupPosition] & LOWER_31BIT_MASK); - final int toChildCount = (int) (mCachedGroupPosInfo[toGroupPosition] & LOWER_31BIT_MASK); - - if (fromChildCount == 0) { - throw new IllegalStateException("moveChildItem(" + - "fromGroupPosition = " + fromGroupPosition + - ", fromChildPosition = " + fromChildPosition + - ", toGroupPosition = " + toGroupPosition + - ", toChildPosition = " + toChildPosition + ") --- may be a bug."); - } - - mCachedGroupPosInfo[fromGroupPosition] = (mCachedGroupPosInfo[fromGroupPosition] & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (fromChildCount - 1); - mCachedGroupPosInfo[toGroupPosition] = (mCachedGroupPosInfo[toGroupPosition] & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (toChildCount + 1); - - if ((mCachedGroupPosInfo[fromGroupPosition] & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= 1; - } - if ((mCachedGroupPosInfo[toGroupPosition] & FLAG_EXPANDED) != 0) { - mExpandedChildCount += 1; - } - - final int minPosition = Math.min(fromGroupPosition, toGroupPosition); - - if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; - } else { - mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; - } - } - - public long getExpandablePosition(int flatPosition) { - if (flatPosition == RecyclerView.NO_POSITION) { - return ExpandableAdapterHelper.NO_EXPANDABLE_POSITION; - } - - final int groupCount = mGroupCount; - - // final int startIndex = 0; - final int startIndex = binarySearchGroupPositionByFlatPosition(mCachedGroupPosInfo, mEndOfCalculatedOffsetGroupPosition, flatPosition); - long expandablePosition = ExpandableAdapterHelper.NO_EXPANDABLE_POSITION; - int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition; - int offset = (startIndex == 0) ? 0 : (int) (mCachedGroupPosInfo[startIndex] >>> 32); - - for (int i = startIndex; i < groupCount; i++) { - final long t = mCachedGroupPosInfo[i]; - - // update offset info - mCachedGroupPosInfo[i] = (((long) offset << 32) | (t & LOWER_32BIT_MASK)); - endOfCalculatedOffsetGroupPosition = i; - - if (offset >= flatPosition) { - // found (group item) - expandablePosition = ExpandableAdapterHelper.getPackedPositionForGroup(i); - break; - } else { - offset += 1; - } - - if ((t & FLAG_EXPANDED) != 0) { - final int childCount = (int) (t & LOWER_31BIT_MASK); - - if ((childCount > 0) && (offset + childCount - 1) >= flatPosition) { - // found (child item) - expandablePosition = ExpandableAdapterHelper.getPackedPositionForChild(i, (flatPosition - offset)); - break; - } else { - offset += childCount; - } - } - } - - mEndOfCalculatedOffsetGroupPosition = Math.max(mEndOfCalculatedOffsetGroupPosition, endOfCalculatedOffsetGroupPosition); - - return expandablePosition; - } - - public int getFlatPosition(long packedPosition) { - if (packedPosition == ExpandableAdapterHelper.NO_EXPANDABLE_POSITION) { - return RecyclerView.NO_POSITION; - } - - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(packedPosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(packedPosition); - final int groupCount = mGroupCount; - - if (!(groupPosition >= 0 && groupPosition < groupCount)) { - return RecyclerView.NO_POSITION; - } - - if (childPosition != RecyclerView.NO_POSITION) { - if (!isGroupExpanded(groupPosition)) { - return RecyclerView.NO_POSITION; - } - } - - // final int startIndex = 0; - final int startIndex = Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition); - int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition; - int offset = (int) (mCachedGroupPosInfo[startIndex] >>> 32); - int flatPosition = RecyclerView.NO_POSITION; - - for (int i = startIndex; i < groupCount; i++) { - final long t = mCachedGroupPosInfo[i]; - - // update offset info - mCachedGroupPosInfo[i] = (((long) offset << 32) | (t & LOWER_32BIT_MASK)); - endOfCalculatedOffsetGroupPosition = i; - - final int childCount = (int) (t & LOWER_31BIT_MASK); - - if (i == groupPosition) { - if (childPosition == RecyclerView.NO_POSITION) { - flatPosition = offset; - } else if (childPosition < childCount) { - flatPosition = (offset + 1) + childPosition; - } - break; - } else { - offset += 1; - - if ((t & FLAG_EXPANDED) != 0) { - offset += childCount; - } - } - } - - mEndOfCalculatedOffsetGroupPosition = Math.max(mEndOfCalculatedOffsetGroupPosition, endOfCalculatedOffsetGroupPosition); - - return flatPosition; - } - - - private static int binarySearchGroupPositionByFlatPosition(long[] array, int endArrayPosition, int flatPosition) { - if (endArrayPosition <= 0) { - return 0; - } - - final int v1 = (int) (array[0] >>> 32); - final int v2 = (int) (array[endArrayPosition] >>> 32); - - if (flatPosition <= v1) { - return 0; - } else if (flatPosition >= v2) { - return endArrayPosition; - } - - int lastS = 0; - int s = 0; - int e = endArrayPosition; - - while (s < e) { - final int mid = (s + e) >>> 1; - final int v = (int) (array[mid] >>> 32); - - if (v < flatPosition) { - lastS = s; - s = mid + 1; - } else { - e = mid; - } - } - - return lastS; - } - - public void removeGroupItem(int groupPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; - - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= (int) (t & LOWER_31BIT_MASK); - mExpandedGroupCount -= 1; - } - - mGroupCount -= 1; - - // shift to forward - for (int i = groupPosition; i < mGroupCount; i++) { - mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1]; - mCachedGroupId[i] = mCachedGroupId[i + 1]; - } - } - - public void removeChildItem(int groupPosition, int childPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; - - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= 1; - } - - mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | ((t & LOWER_31BIT_MASK) - 1); - } -} 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 deleted file mode 100644 index 0d8a3e9b42f..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java +++ /dev/null @@ -1,658 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; - -import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter; -import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder; -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.swipeable.SwipeableItemAdapter; -import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter; -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; - -class ExpandableRecyclerViewWrapperAdapter - extends BaseWrapperAdapter - implements DraggableItemAdapter, - SwipeableItemAdapter { - private static final String TAG = "ARVExpandableWrapper"; - - private static final int VIEW_TYPE_FLAG_IS_GROUP = ExpandableAdapterHelper.VIEW_TYPE_FLAG_IS_GROUP; - - private static final int STATE_FLAG_INITIAL_VALUE = -1; - - private ExpandableItemAdapter mExpandableItemAdapter; - private RecyclerViewExpandableItemManager mExpandableListManager; - private ExpandablePositionTranslator mPositionTranslator; - private int mDraggingItemGroupPosition = RecyclerView.NO_POSITION; - - public ExpandableRecyclerViewWrapperAdapter(RecyclerViewExpandableItemManager manager, RecyclerView.Adapter adapter, int[] expandedItemsSavedState) { - super(adapter); - - mExpandableItemAdapter = getExpandableItemAdapter(adapter); - if (mExpandableItemAdapter == null) { - throw new IllegalArgumentException("adapter does not implement RecyclerViewExpandableListManager"); - } - - if (manager == null) { - throw new IllegalArgumentException("manager cannot be null"); - } - - mExpandableListManager = manager; - - mPositionTranslator = new ExpandablePositionTranslator(); - mPositionTranslator.build(mExpandableItemAdapter); - - if (expandedItemsSavedState != null) { - mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState); - } - } - - @Override - protected void onRelease() { - super.onRelease(); - - mExpandableItemAdapter = null; - mExpandableListManager = null; - } - - @Override - public int getItemCount() { - return mPositionTranslator.getItemCount(); - } - - @Override - public long getItemId(int position) { - if (mExpandableItemAdapter == null) { - return RecyclerView.NO_ID; - } - - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - final long groupId = mExpandableItemAdapter.getGroupId(groupPosition); - return ExpandableAdapterHelper.getCombinedGroupId(groupId); - } else { - final long groupId = mExpandableItemAdapter.getGroupId(groupPosition); - final long childId = mExpandableItemAdapter.getChildId(groupPosition, childPosition); - return ExpandableAdapterHelper.getCombinedChildId(groupId, childId); - } - } - - @Override - public int getItemViewType(int position) { - if (mExpandableItemAdapter == null) { - return 0; - } - - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - final int type; - - if (childPosition == RecyclerView.NO_POSITION) { - type = mExpandableItemAdapter.getGroupItemViewType(groupPosition); - } else { - type = mExpandableItemAdapter.getChildItemViewType(groupPosition, childPosition); - } - - if ((type & VIEW_TYPE_FLAG_IS_GROUP) != 0) { - throw new IllegalStateException("Illegal view type (type = " + Integer.toHexString(type) + ")"); - } - - return (childPosition == RecyclerView.NO_POSITION) ? (type | VIEW_TYPE_FLAG_IS_GROUP) : (type); - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - if (mExpandableItemAdapter == null) { - return null; - } - - final int maskedViewType = (viewType & (~VIEW_TYPE_FLAG_IS_GROUP)); - - final RecyclerView.ViewHolder holder; - - if ((viewType & VIEW_TYPE_FLAG_IS_GROUP) != 0) { - holder = mExpandableItemAdapter.onCreateGroupViewHolder(parent, maskedViewType); - } else { - holder = mExpandableItemAdapter.onCreateChildViewHolder(parent, maskedViewType); - } - - if (holder instanceof ExpandableItemViewHolder) { - ((ExpandableItemViewHolder) holder).setExpandStateFlags(STATE_FLAG_INITIAL_VALUE); - } - - return holder; - - } - - @SuppressWarnings("unchecked") - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - if (mExpandableItemAdapter == null) { - return; - } - - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - final int viewType = (holder.getItemViewType() & (~VIEW_TYPE_FLAG_IS_GROUP)); - - // update flags - int flags = 0; - - if (childPosition == RecyclerView.NO_POSITION) { - flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_GROUP; - } else { - flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_CHILD; - } - - if (mPositionTranslator.isGroupExpanded(groupPosition)) { - flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_EXPANDED; - } - - safeUpdateExpandStateFlags(holder, flags); - - correctItemDragStateFlags(holder, groupPosition); - - if (childPosition == RecyclerView.NO_POSITION) { - mExpandableItemAdapter.onBindGroupViewHolder(holder, groupPosition, viewType); - } else { - mExpandableItemAdapter.onBindChildViewHolder(holder, groupPosition, childPosition, viewType); - } - } - - private void rebuildPositionTranslator() { - if (mPositionTranslator != null) { - int [] savedState = mPositionTranslator.getSavedStateArray(); - mPositionTranslator.build(mExpandableItemAdapter); - mPositionTranslator.restoreExpandedGroupItems(savedState); - } - } - - @Override - protected void onHandleWrappedAdapterChanged() { - rebuildPositionTranslator(); - super.onHandleWrappedAdapterChanged(); - } - - @Override - protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) { - super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount); - } - - @Override - protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) { - rebuildPositionTranslator(); - super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount); - } - - @Override - protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) { - if (itemCount == 1) { - final long expandablePosition = mPositionTranslator.getExpandablePosition(positionStart); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - mPositionTranslator.removeGroupItem(groupPosition); - } else { - mPositionTranslator.removeChildItem(groupPosition, childPosition); - } - } else { - rebuildPositionTranslator(); - } - - super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount); - } - - @Override - protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) { - rebuildPositionTranslator(); - super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount); - } - - @SuppressWarnings("unchecked") - @Override - public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int x, int y) { - if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { - return false; - } - - final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - - final int flatPosition = holder.getPosition(); - final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - boolean canStart; - if (childPosition == RecyclerView.NO_POSITION) { - canStart = adapter.onCheckGroupCanStartDrag(holder, groupPosition, x, y); - } else { - canStart = adapter.onCheckChildCanStartDrag(holder, groupPosition, childPosition, x, y); - } - - mDraggingItemGroupPosition = (canStart) ? groupPosition : RecyclerView.NO_POSITION; - - return canStart; - } - - @SuppressWarnings("unchecked") - @Override - public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder) { - if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { - return null; - } - - if (mExpandableItemAdapter.getGroupCount() < 1) { - return null; - } - - final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - - final int flatPosition = holder.getPosition(); - 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); - - if (groupRange == 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()); - final int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition); - int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition); - - if (groupRange.getEnd() > groupPosition) { - end += mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); - } - - return new ItemDraggableRange(start, end); - } - } else { - // child - final ItemDraggableRange groupRange = 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) { - 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()); - final int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition) - + mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); - - int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition) + 1; - - start = Math.min(start, end); - - return new ItemDraggableRange(start, end); - } - } - } - - @Override - public void onMoveItem(int fromPosition, int toPosition) { - if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { - return; - } - - mDraggingItemGroupPosition = RecyclerView.NO_POSITION; - - if (fromPosition == toPosition) { - return; - } - - final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - - final long expandableFromPosition = mPositionTranslator.getExpandablePosition(fromPosition); - final int fromGroupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandableFromPosition); - final int fromChildPosition = ExpandableAdapterHelper.getPackedPositionChild(expandableFromPosition); - - final long expandableToPosition = mPositionTranslator.getExpandablePosition(toPosition); - final int toGroupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandableToPosition); - final int toChildPosition = ExpandableAdapterHelper.getPackedPositionChild(expandableToPosition); - - final boolean fromIsGroup = (fromChildPosition == RecyclerView.NO_POSITION); - final boolean toIsGroup = (toChildPosition == RecyclerView.NO_POSITION); - - int actualToFlatPosition = fromPosition; - - if (fromIsGroup && toIsGroup) { - adapter.onMoveGroupItem(fromGroupPosition, toGroupPosition); - mPositionTranslator.moveGroupItem(fromGroupPosition, toGroupPosition); - actualToFlatPosition = toPosition; - } else if (!fromIsGroup && !toIsGroup) { - int modToChildPosition; - - // correct child position - if (fromGroupPosition == toGroupPosition) { - modToChildPosition = toChildPosition; - } else { - if (fromPosition < toPosition) { - modToChildPosition = toChildPosition + 1; - } else { - modToChildPosition = toChildPosition; - } - } - - actualToFlatPosition = mPositionTranslator.getFlatPosition( - ExpandableAdapterHelper.getPackedPositionForChild(fromGroupPosition, modToChildPosition)); - - adapter.onMoveChildItem(fromGroupPosition, fromChildPosition, toGroupPosition, modToChildPosition); - mPositionTranslator.moveChildItem(fromGroupPosition, fromChildPosition, toGroupPosition, modToChildPosition); - } else if (!fromIsGroup /*&& toIsGroup NOTE: toIsGroup is always true here*/) { - int modToGroupPosition; - int modToChildPosition; - - if (toPosition < fromPosition) { - if (toGroupPosition == 0) { - // insert at the top - modToGroupPosition = toGroupPosition; - modToChildPosition = 0; - } else { - // insert at the end - modToGroupPosition = toGroupPosition - 1; - modToChildPosition = mPositionTranslator.getChildCount(modToGroupPosition); - } - } else { - if (mPositionTranslator.isGroupExpanded(toGroupPosition)) { - // insert at the top - modToGroupPosition = toGroupPosition; - modToChildPosition = 0; - } else { - // insert at the end - modToGroupPosition = toGroupPosition; - modToChildPosition = mPositionTranslator.getChildCount(modToGroupPosition); - } - } - - if (fromGroupPosition == modToGroupPosition) { - final int lastIndex = Math.max(0, mPositionTranslator.getChildCount(modToGroupPosition) - 1); - modToChildPosition = Math.min(modToChildPosition, lastIndex); - } - - if (!((fromGroupPosition == modToGroupPosition) && (fromChildPosition == modToChildPosition))) { - if (mPositionTranslator.isGroupExpanded(toGroupPosition)) { - actualToFlatPosition = toPosition; - } else { - actualToFlatPosition = RecyclerView.NO_POSITION; - } - - adapter.onMoveChildItem(fromGroupPosition, fromChildPosition, modToGroupPosition, modToChildPosition); - mPositionTranslator.moveChildItem(fromGroupPosition, fromChildPosition, modToGroupPosition, modToChildPosition); - } - } else { // if (fromIsGroup && !toIsGroup) - if (fromGroupPosition != toGroupPosition) { - actualToFlatPosition = mPositionTranslator.getFlatPosition(ExpandableAdapterHelper.getPackedPositionForGroup(toGroupPosition)); - - adapter.onMoveGroupItem(fromGroupPosition, toGroupPosition); - mPositionTranslator.moveGroupItem(fromGroupPosition, toGroupPosition); - } - } - - if (actualToFlatPosition != fromPosition) { - if (actualToFlatPosition != RecyclerView.NO_POSITION) { - notifyItemMoved(fromPosition, actualToFlatPosition); - } else { - notifyItemRemoved(fromPosition); - } - } - } - - @SuppressWarnings("unchecked") - @Override - public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, 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 groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - return adapter.onGetGroupItemSwipeReactionType(holder, groupPosition, x, y); - } else { - return adapter.onGetChildItemSwipeReactionType(holder, groupPosition, childPosition, x, y); - } - } - - @SuppressWarnings("unchecked") - @Override - public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int type) { - if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { - return; - } - - final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - adapter.onSetGroupItemSwipeBackground(holder, groupPosition, type); - } else { - adapter.onSetChildItemSwipeBackground(holder, groupPosition, childPosition, type); - } - } - - @SuppressWarnings("unchecked") - @Override - public int onSwipeItem(RecyclerView.ViewHolder holder, 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 groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - return adapter.onSwipeGroupItem(holder, groupPosition, result); - } else { - return adapter.onSwipeChildItem(holder, groupPosition, childPosition, result); - } - } - - @SuppressWarnings("unchecked") - @Override - public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int result, int reaction) { - if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { - return; - } - - final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition == RecyclerView.NO_POSITION) { - adapter.onPerformAfterSwipeGroupReaction(holder, groupPosition, result, reaction); - } else { - 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) { - if (mExpandableItemAdapter == null) { - return false; - } - - final int position = holder.getPosition(); - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); - final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); - final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); - - if (childPosition != RecyclerView.NO_POSITION) { - return false; - } - - final boolean expand = !(mPositionTranslator.isGroupExpanded(groupPosition)); - - boolean result = mExpandableItemAdapter.onCheckCanExpandOrCollapseGroup(holder, groupPosition, x, y, expand); - - if (!result) { - return false; - } - - if (expand) { - expandGroup(groupPosition); - } else { - collapseGroup(groupPosition); - } - - return true; - } - - /*package*/ boolean collapseGroup(int groupPosition) { - if (!mPositionTranslator.isGroupExpanded(groupPosition)) { - return false; - } - - if (mPositionTranslator.collapseGroup(groupPosition)) { - final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); - final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); - final int childCount = mPositionTranslator.getChildCount(groupPosition); - - notifyItemRangeRemoved(flatPosition + 1, childCount); - } - - - { - final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); - final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); - - notifyItemChanged(flatPosition); - } - - return true; - } - - /*package*/ boolean expandGroup(int groupPosition) { - if (mPositionTranslator.isGroupExpanded(groupPosition)) { - return false; - } - - - if (mPositionTranslator.expandGroup(groupPosition)) { - final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); - final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); - final int childCount = mPositionTranslator.getChildCount(groupPosition); - - notifyItemRangeInserted(flatPosition + 1, childCount); - } - - { - final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); - final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); - - notifyItemChanged(flatPosition); - } - - return true; - } - - /*package*/ boolean isGroupExpanded(int groupPosition) { - return mPositionTranslator.isGroupExpanded(groupPosition); - } - - /*package*/ long getExpandablePosition(int flatPosition) { - return mPositionTranslator.getExpandablePosition(flatPosition); - } - - /*package*/ int getFlatPosition(long packedPosition) { - return mPositionTranslator.getFlatPosition(packedPosition); - } - - /*package*/ int[] getExpandedItemsSavedStateArray() { - if (mPositionTranslator != null) { - return mPositionTranslator.getSavedStateArray(); - } else { - return null; - } - } - - private static ExpandableItemAdapter getExpandableItemAdapter(RecyclerView.Adapter adapter) { - return WrapperAdapterUtils.findWrappedAdapter(adapter, ExpandableItemAdapter.class); - } - - private static void safeUpdateExpandStateFlags(RecyclerView.ViewHolder holder, int flags) { - if (!(holder instanceof ExpandableItemViewHolder)) { - return; - } - - final ExpandableItemViewHolder holder2 = (ExpandableItemViewHolder) holder; - - final int curFlags = holder2.getExpandStateFlags(); - final int mask = ~RecyclerViewExpandableItemManager.STATE_FLAG_IS_UPDATED; - - // append UPDATED flag - if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) { - flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_UPDATED; - } - - holder2.setExpandStateFlags(flags); - } - - private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition) { - if (!(holder instanceof DraggableItemViewHolder)) { - return; - } - - final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder; - - final int flags = holder2.getDragStateFlags(); - - 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); - } - } - } -} 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 deleted file mode 100644 index 8f8a6c88a24..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 android.support.v7.widget.RecyclerView; - -public interface ExpandableSwipeableItemAdapter { - /** - * Called when user is attempt to swipe the group item. - * - * @param holder The ViewHolder which is associated to item user is attempt to start swiping. - * @param groupPosition Group position. - * @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 Reaction type. Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT} - */ - int onGetGroupItemSwipeReactionType(GVH holder, int groupPosition, int x, int y); - - /** - * Called when user is attempt to swipe the child item. - * - * @param holder The ViewHolder which is associated to item user is attempt to start swiping. - * @param groupPosition Group position. - * @param childPosition Child position. - * @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 Reaction type. Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT} - */ - int onGetChildItemSwipeReactionType(CVH holder, int groupPosition, int childPosition, int x, int y); - - /** - * Called when sets background of the swiping group item. - * - * @param holder The ViewHolder which is associated to the swiping item. - * @param groupPosition Group position. - * @param type Background type. One of the - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}. - */ - void onSetGroupItemSwipeBackground(GVH holder, int groupPosition, int type); - - - /** - * Called when sets background of the swiping child item. - * - * @param holder The ViewHolder which is associated to the swiping item. - * @param groupPosition Group position. - * @param childPosition Child position. - * @param type Background type. One of the - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}. - */ - void onSetChildItemSwipeBackground(CVH holder, int groupPosition, int childPosition, int type); - - /** - * Called when group item is swiped. - * - * *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 groupPosition Group position. - * @param result The result code of user's swipe operation. - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * - * @return Reaction type of after swiping. - * One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. - */ - int onSwipeGroupItem(GVH holder, int groupPosition, int result); - - /** - * Called when child item is swiped. - * - * *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 groupPosition Group position. - * @param childPosition Child position. - * @param result The result code of user's swipe operation. - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * - * @return Reaction type of after swiping. - * One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. - */ - int onSwipeChildItem(CVH holder, int groupPosition, int childPosition, int result); - - /** - * Called after {@link #onSwipeGroupItem(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 groupPosition Group position. - * @param result The result code of user's swipe operation. - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * @param reaction Reaction type. One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. - */ - void onPerformAfterSwipeGroupReaction(GVH holder, int groupPosition, int result, int reaction); - - - /** - * Called after {@link #onSwipeChildItem(android.support.v7.widget.RecyclerView.ViewHolder, int, 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 groupPosition Group position. - * @param childPosition Child position. - * @param result The result code of user's swipe operation. - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * @param reaction Reaction type. One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or - * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. - */ - void onPerformAfterSwipeChildReaction(CVH holder, int groupPosition, int childPosition, int result, int reaction); -} 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 deleted file mode 100644 index 206bf9d81e6..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java +++ /dev/null @@ -1,472 +0,0 @@ -/* - * 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 android.os.Parcel; -import android.os.Parcelable; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; - -/** - * Provides item swipe operation for {@link android.support.v7.widget.RecyclerView} - */ -public class RecyclerViewExpandableItemManager { - private static final String TAG = "ARVExpandableItemMgr"; - - /** - * Packed position version of {@link android.support.v7.widget.RecyclerView#NO_POSITION} - */ - public static final long NO_EXPANDABLE_POSITION = ExpandableAdapterHelper.NO_EXPANDABLE_POSITION; - - - /** - * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods. - * Indicates that this ViewHolder is associated to group item. - */ - @SuppressWarnings("PointlessBitwiseExpression") - public static final int STATE_FLAG_IS_GROUP = (1 << 0); - - /** - * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods. - * Indicates that this ViewHolder is associated to child item. - */ - public static final int STATE_FLAG_IS_CHILD = (1 << 1); - - /** - * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods. - * Indicates that this is a group item. - */ - public static final int STATE_FLAG_IS_EXPANDED = (1 << 2); - - /** - * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods. - * If this flag is set, some other flags are changed and require to apply. - */ - public static final int STATE_FLAG_IS_UPDATED = (1 << 31); - - // --- - - private SavedState mSavedState; - - private RecyclerView mRecyclerView; - private ExpandableRecyclerViewWrapperAdapter mAdapter; - private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; - - private long mTouchedItemId = RecyclerView.NO_ID; - private int mTouchSlop; - private int mInitialTouchX; - private int mInitialTouchY; - - /** - * Constructor. - * - * @param savedState The saved state object which is obtained from the {@link #getSavedState()} method. - */ - public RecyclerViewExpandableItemManager(Parcelable savedState) { - mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - return RecyclerViewExpandableItemManager.this.onInterceptTouchEvent(rv, e); - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - } - }; - - if (savedState instanceof SavedState) { - mSavedState = (SavedState) savedState; - } - } - - /** - * 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. - * - * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set - * 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 - */ - public void attachRecyclerView(RecyclerView rv) { - if (rv == null) { - throw new IllegalArgumentException("RecyclerView cannot be null"); - } - - if (isReleased()) { - throw new IllegalStateException("Accessing released object"); - } - - if (mRecyclerView != null) { - throw new IllegalStateException("RecyclerView instance has already been set"); - } - - mRecyclerView = rv; - mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener); - mTouchSlop = ViewConfiguration.get(mRecyclerView.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); - } - mInternalUseOnItemTouchListener = null; - mRecyclerView = null; - mSavedState = null; - } - - /** - * Create wrapped adapter. - * - * @param adapter The target adapter. - * - * @return Wrapped adapter which is associated to this {@link RecyclerViewExpandableItemManager} instance. - */ - @SuppressWarnings("unchecked") - public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) { - if (mAdapter != null) { - throw new IllegalStateException("already have a wrapped adapter"); - } - - int [] adapterSavedState = (mSavedState != null) ? mSavedState.adapterSavedState : null; - mSavedState = null; - - mAdapter = new ExpandableRecyclerViewWrapperAdapter(this, adapter, adapterSavedState); - - return mAdapter; - } - - /** - * Gets saved state object in order to restore the internal state. - * - * Call this method in Activity/Fragment's onSavedInstance() and save to the bundle. - * - * @return The Parcelable object which stores information need to restore the internal states. - */ - public Parcelable getSavedState() { - int [] adapterSavedState = null; - - if (mAdapter != null) { - adapterSavedState = mAdapter.getExpandedItemsSavedStateArray(); - } - - return new SavedState(adapterSavedState); - } - - /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - if (mAdapter == null) { - return false; - } - - final int action = MotionEventCompat.getActionMasked(e); - - switch (action) { - case MotionEvent.ACTION_DOWN: - handleActionDown(rv, e); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (handleActionUpOrCancel(rv, e)) { - return true; - } - break; - } - - return false; - } - - private void handleActionDown(RecyclerView rv, MotionEvent e) { - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY()); - - mInitialTouchX = (int) (e.getX() + 0.5f); - mInitialTouchY = (int) (e.getY() + 0.5f); - - if (holder != null) { - mTouchedItemId = holder.getItemId(); - } else { - mTouchedItemId = RecyclerView.NO_ID; - } - } - - private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) { - final long touchedItemId = mTouchedItemId; - final int initialTouchX = mInitialTouchX; - final int initialTouchY = mInitialTouchY; - - mTouchedItemId = RecyclerView.NO_ID; - mInitialTouchX = 0; - mInitialTouchY = 0; - - if (!((touchedItemId != RecyclerView.NO_ID) && (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP))) { - return false; - } - - final int touchX = (int) (e.getX() + 0.5f); - final int touchY = (int) (e.getY() + 0.5f); - - final int diffX = touchX - initialTouchX; - final int diffY = touchY - initialTouchY; - - if (!((Math.abs(diffX) < mTouchSlop) && (Math.abs(diffY) < mTouchSlop))) { - return false; - } - - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY()); - - if (!((holder != null) && (holder.getItemId() == touchedItemId))) { - 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); - } - - /** - * Expand a group. - * - * @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) - */ - public boolean expandGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.expandGroup(groupPosition); - } - - /** - * Collapse a group. - * - * @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) - */ - public boolean collapseGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.collapseGroup(groupPosition); - } - - /** - * Converts a flat position (the raw position of an item in the list) to a group and/or child position - * (represented in a packed position). Use {@link #getPackedPositionChild(long)}, {@link #getPackedPositionGroup(long)} to unpack. - * - * @param flatPosition The flat position to be converted - * - * @return The group and/or child position for the given flat position in packed position representation. - */ - public long getExpandablePosition(int flatPosition) { - if (mAdapter == null) { - return ExpandableAdapterHelper.NO_EXPANDABLE_POSITION; - } - return mAdapter.getExpandablePosition(flatPosition); - } - - /** - * Converts a group and/or child position to a flat position. - * - * @param packedPosition The group and/or child position to be converted in packed position representation. - * - * @return The group and/or child position for the given flat position in packed position representation. - */ - public int getFlatPosition(long packedPosition) { - if (mAdapter == null) { - return RecyclerView.NO_POSITION; - } - return mAdapter.getFlatPosition(packedPosition); - } - - /** - * Gets the child position from a packed position. - * To get the group that this child belongs to, use {@link #getPackedPositionGroup(long)}. - * See {@link #getPackedPositionForChild(int, int)}. - * - * @param packedPosition The packed position from which the child position will be returned. - * - * @return The child position portion of the packed position. If this does not contain a child, returns {@link android.support.v7.widget.RecyclerView#NO_POSITION}. - */ - public static int getPackedPositionChild(long packedPosition) { - return ExpandableAdapterHelper.getPackedPositionChild(packedPosition); - } - - /** - * 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. - * The two positions are packed into a single long which can be unpacked using {@link #getPackedPositionChild(long)} and - * {@link #getPackedPositionGroup(long)}. - * - * @param groupPosition The child's parent group's position - * @param childPosition The child position within the group - * - * @return The packed position representation of the child (and parent group). - */ - public static long getPackedPositionForChild(int groupPosition, int childPosition) { - return ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition); - } - - /** - * Returns the packed position representation of a group's position. See {@link #getPackedPositionForChild(int, int)}. - * - * @param groupPosition The child's parent group's position. - * - * @return The packed position representation of the group. - */ - public static long getPackedPositionForGroup(int groupPosition) { - return ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); - } - - /** - * Gets the group position from a packed position. See {@link #getPackedPositionForChild(int, int)}. - * - * @param packedPosition The packed position from which the group position will be returned. - * - * @return THe group position of the packed position. If this does not contain a group, returns {@link android.support.v7.widget.RecyclerView#NO_POSITION}. - */ - public static int getPackedPositionGroup(long packedPosition) { - return ExpandableAdapterHelper.getPackedPositionGroup(packedPosition); - } - - /** - * Whether the given group is currently expanded. - * - * @param groupPosition The group to check - * - * @return Whether the group is currently expanded - */ - public boolean isGroupExpanded(int groupPosition) { - return (mAdapter != null) && mAdapter.isGroupExpanded(groupPosition); - } - - /** - * Gets combined ID for child item. - * - * bit 0-31: Lower 32 bits of the childId - * bit 32-62: Lower 31 bits of the groupId - * bit 63: reserved - * - * @param groupId The ID of the group that contains the child. - * @param childId The ID of the child. - * - * @return The unique ID of the child across all groups and children in the list - */ - public static long getCombinedChildId(long groupId, long childId) { - return ExpandableAdapterHelper.getCombinedChildId(groupId, childId); - } - - /** - * Gets combined ID for child item. - * - * bit 0-31: all bits are set to 1 - * bit 32-62: Lower 31 bits of the groupId - * bit 63: reserved - * - * @param groupId The ID of the group that contains the child. - * - * @return The unique ID of the child across all groups and children in the list - */ - public static long getCombinedGroupId(long groupId) { - return ExpandableAdapterHelper.getCombinedGroupId(groupId); - } - - /** - * Checks whether the passed view type is a group's one. - * - * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()}) - * - * @return True for the a group view type, otherwise false - */ - public static boolean isGroupViewType(int rawViewType) { - return ExpandableAdapterHelper.isGroupViewType(rawViewType); - } - - /** - * Gets group view type from a raw view type. - * - * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()}) - * - * @return Group view type for the given raw view type. - */ - public static int getGroupViewType(int rawViewType) { - return ExpandableAdapterHelper.getGroupViewType(rawViewType); - } - - /** - * Gets child view type from a raw view type. - * - * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()}) - * - * @return Child view type for the given raw view type. - */ - public static int getChildViewType(int rawViewType) { - return ExpandableAdapterHelper.getChildViewType(rawViewType); - } - - public static class SavedState implements Parcelable { - final int [] adapterSavedState; - - public SavedState(int[] adapterSavedState) { - this.adapterSavedState = adapterSavedState; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeIntArray(this.adapterSavedState); - } - - private SavedState(Parcel in) { - this.adapterSavedState = in.createIntArray(); - } - - public static final Creator CREATOR = new Creator() { - public SavedState createFromParcel(Parcel source) { - return new SavedState(source); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } -} 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 deleted file mode 100644 index 925a006146c..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.swipeable; - -import android.annotation.SuppressLint; -import android.graphics.Rect; -import android.os.Build; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v4.view.ViewPropertyAnimatorListener; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; - -import java.util.ArrayList; -import java.util.List; - -public class ItemSlidingAnimator { - private static final String TAG = "ItemSlidingAnimator"; - - private Interpolator mSlideToDefaultPositionAnimationInterpolator = new AccelerateDecelerateInterpolator(); - private Interpolator mSlideToOutsideOfWindowAnimationInterpolator = new AccelerateInterpolator(0.8f); - private List mActive; - private int[] mTmpLocation = new int[2]; - private Rect mTmpRect = new Rect(); - private int mImmediatelySetTranslationThreshold; - - public ItemSlidingAnimator() { - mActive = new ArrayList<>(); - } - - public void slideToDefaultPosition(RecyclerView.ViewHolder holder, boolean shouldAnimate, long duration) { - slideToSpecifiedPositionInternal(holder, 0, shouldAnimate, duration); - } - - public void slideToOutsideOfWindow(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { - slideToOutsideOfWindowInternal(holder, toLeft, shouldAnimate, duration); - } - - public void slideToSpecifiedPosition(RecyclerView.ViewHolder holder, float position) { - slideToSpecifiedPositionInternal(holder, position, false, 0); - } - - private boolean slideToSpecifiedPositionInternal(RecyclerView.ViewHolder holder, float position, boolean shouldAnimate, long duration) { - final int translationX; - - duration = (shouldAnimate) ? duration : 0; - - if (position != 0.0f) { - final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - - translationX = (int) (containerView.getWidth() * position + 0.5f); - } else { - translationX = 0; - } - - return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator); - } - - private boolean slideToOutsideOfWindowInternal(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { - if (!(holder instanceof SwipeableItemViewHolder)) { - return false; - } - - final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - final ViewGroup parent = (ViewGroup) containerView.getParent(); - - if (parent == null) { - return false; - } - - final int left = containerView.getLeft(); - final int right = containerView.getRight(); - final int width = right - left; - final boolean parentIsShown = parent.isShown(); - - parent.getWindowVisibleDisplayFrame(mTmpRect); - - final int translateX; - if ((width == 0) || !parentIsShown) { - // not measured yet or not shown - translateX = (toLeft) ? (-mTmpRect.width()) : (mTmpRect.width()); - shouldAnimate = false; - } else { - parent.getLocationInWindow(mTmpLocation); - - if (toLeft) { - translateX = -(mTmpLocation[0] + width); - } else { - translateX = mTmpRect.width() - (mTmpLocation[0] - left); - } - } - - if (shouldAnimate) { - shouldAnimate = containerView.isShown(); - } - - duration = (shouldAnimate) ? duration : 0; - - return animateSlideInternalCompat(holder, translateX, duration, mSlideToOutsideOfWindowAnimationInterpolator); - } - - private boolean animateSlideInternalCompat(RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) { - if (supportsViewPropertyAnimator()) { - return animateSlideInternal(holder, translationX, duration, interpolator); - } else { - return animateSlideInternalPreHoneycomb(holder, translationX); - } - } - - @SuppressLint("RtlHardcoded") - private boolean animateSlideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) { - if (!(holder instanceof SwipeableItemViewHolder)) { - return false; - } - - final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - - final ViewGroup.LayoutParams lp = containerView.getLayoutParams(); - if (lp instanceof ViewGroup.MarginLayoutParams) { - final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; - mlp.leftMargin = translationX; - mlp.rightMargin = -translationX; - - if (lp instanceof FrameLayout.LayoutParams) { - ((FrameLayout.LayoutParams) lp).gravity = Gravity.TOP | Gravity.LEFT; - } - containerView.setLayoutParams(mlp); - } else { - Log.w(TAG, "should use MarginLayoutParams supported view for compatibility on Android 2.3"); - } - - return false; - } - - private boolean animateSlideInternal(final RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) { - if (!(holder instanceof SwipeableItemViewHolder)) { - return false; - } - - final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - - final int prevTranslationX = (int) (ViewCompat.getTranslationX(containerView) + 0.5f); - - endAnimation(holder); - - final int toX = translationX; - - if (duration == 0 || Math.abs(toX - prevTranslationX) <= mImmediatelySetTranslationThreshold) { - ViewCompat.setTranslationX(containerView, toX); - return false; - } - - ViewCompat.setTranslationX(containerView, prevTranslationX); - - final ViewPropertyAnimatorCompat animator = ViewCompat.animate(containerView); - - animator.setDuration(duration); - if (interpolator != null) { - animator.setInterpolator(interpolator); - } - animator.translationX(toX); - animator.setListener(new ViewPropertyAnimatorListener() { - @Override - public void onAnimationStart(View view) { - } - - @Override - public void onAnimationEnd(View view) { - animator.setListener(null); - mActive.remove(holder); - ViewCompat.setTranslationX(view, toX); - } - - @Override - public void onAnimationCancel(View view) { - } - }); - - mActive.add(holder); - - animator.start(); - - return true; - } - - public void endAnimation(RecyclerView.ViewHolder holder) { - if (!(holder instanceof SwipeableItemViewHolder)) { - return; - } - - final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - - ViewCompat.animate(containerView).cancel(); - - if (mActive.remove(holder)) { - throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [slide]"); - } - } - - public void endAnimations() { - for (int i = mActive.size() - 1; i >= 0; i--) { - final RecyclerView.ViewHolder holder = mActive.get(i); - endAnimation(holder); - } - } - - public boolean isRunning(RecyclerView.ViewHolder holder) { - return mActive.contains(holder); - } - - public boolean isRunning() { - return !(mActive.isEmpty()); - } - - public int getImmediatelySetTranslationThreshold() { - return mImmediatelySetTranslationThreshold; - } - - public void setImmediatelySetTranslationThreshold(int threshold) { - mImmediatelySetTranslationThreshold = threshold; - } - - 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/RecyclerViewSwipeManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java deleted file mode 100644 index 810f98835ee..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java +++ /dev/null @@ -1,750 +0,0 @@ -/* - * 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.swipeable; - -import android.graphics.Rect; -import android.os.Build; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; - -import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; - -/** - * Provides item swipe operation for {@link android.support.v7.widget.RecyclerView} - */ -@SuppressWarnings("PointlessBitwiseExpression") -public class RecyclerViewSwipeManager { - private static final String TAG = "ARVSwipeManager"; - - /** - * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * None. (internal default value, this value is not used for the argument) - */ - public static final int RESULT_NONE = 0; - - /** - * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Canceled. - */ - public static final int RESULT_CANCELED = 1; - - /** - * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Swipe left performed. - */ - public static final int RESULT_SWIPED_LEFT = 2; - - /** - * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Swipe right performed. - */ - public static final int RESULT_SWIPED_RIGHT = 3; - - // --- - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can not swipe left" (completely no reactions) - */ - public static final int REACTION_CAN_NOT_SWIPE_LEFT = (0 << 0); - - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can not swipe left" (not swipeable, but rubber-band effect applied) - */ - public static final int REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT = (1 << 0); - - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can swipe left" - */ - public static final int REACTION_CAN_SWIPE_LEFT = (2 << 0); - - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can not swipe right" (completely no reactions) - */ - public static final int REACTION_CAN_NOT_SWIPE_RIGHT = (0 << 16); - - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can not swipe right" (not swipeable, but rubber-band effect applied) - */ - public static final int REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT = (1 << 16); - - /** - * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. - * - * Indicates "can swipe right" - */ - public static final int REACTION_CAN_SWIPE_RIGHT = (2 << 16); - - /** - * Convenient constant value: Equals to {@link #REACTION_CAN_NOT_SWIPE_LEFT} | {@link #REACTION_CAN_NOT_SWIPE_RIGHT} - */ - public static final int REACTION_CAN_NOT_SWIPE_BOTH = - REACTION_CAN_NOT_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT; - - /** - * Convenient constant value: Equals to {@link #REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT} | {@link #REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT} - */ - public static final int REACTION_CAN_NOT_SWIPE_BOTH_WITH_RUBBER_BAND_EFFECT = - REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT | - REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; - - /** - * Convenient constant value: Equals to {@link #REACTION_CAN_SWIPE_LEFT} | {@link #REACTION_CAN_SWIPE_RIGHT} - */ - public static final int REACTION_CAN_SWIPE_BOTH = - REACTION_CAN_SWIPE_LEFT | REACTION_CAN_SWIPE_RIGHT; - - // --- - /** - * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. - * - * Background image for the neutral (= not swiping) item. - */ - public static final int DRAWABLE_SWIPE_NEUTRAL_BACKGROUND = 0; - - /** - * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. - * - * Background image for the swiping-left item. - */ - public static final int DRAWABLE_SWIPE_LEFT_BACKGROUND = 1; - - /** - * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. - * - * Background image for the swiping-right item. - */ - public static final int DRAWABLE_SWIPE_RIGHT_BACKGROUND = 2; - - // --- - /** - * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods. - * Represents perform nothing. - */ - public static final int AFTER_SWIPE_REACTION_DEFAULT = 0; - - /** - * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods. - * Represents remove the swiped item. - */ - public static final int AFTER_SWIPE_REACTION_REMOVE_ITEM = 1; - - /** - * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods. - * Represents that the item moved to swiped direction. - */ - public static final int AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION = 2; - - // --- - /** - * Special value for the {@link SwipeableItemViewHolder#setSwipeItemSlideAmount(float)} and {@link SwipeableItemViewHolder#getSwipeItemSlideAmount()} methods. - * Indicates that this item is pinned to LEFT of the window. - */ - public static final float OUTSIDE_OF_THE_WINDOW_LEFT = -Float.MAX_VALUE; - - /** - * Special value for the {@link SwipeableItemViewHolder#setSwipeItemSlideAmount(float)} and {@link SwipeableItemViewHolder#getSwipeItemSlideAmount()} methods. - * Indicates that this item is pinned to RIGHT the window. - */ - public static final float OUTSIDE_OF_THE_WINDOW_RIGHT = Float.MAX_VALUE; - - - // --- - /** - * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods. - * Indicates that currently performing swiping. - */ - public static final int STATE_FLAG_SWIPING = (1 << 0); - - /** - * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods. - * Indicates that this item is being swiped. - */ - public static final int STATE_FLAG_IS_ACTIVE = (1 << 1); - - /** - * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods. - * If this flag is set, some other flags are changed and require to apply. - */ - public static final int STATE_FLAG_IS_UPDATED = (1 << 31); - - // --- - - private static final int MIN_DISTANCE_TOUCH_SLOP_MUL = 10; - private static final int SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP = 8; - - private static final boolean LOCAL_LOGV = false; - private static final boolean LOCAL_LOGD = false; - - private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; - private RecyclerView mRecyclerView; - - private long mReturnToDefaultPositionAnimationDuration = 300; - private long mMoveToOutsideWindowAnimationDuration = 200; - - private int mTouchSlop; - private int mMinFlingVelocity; // [pixels per second] - private int mMaxFlingVelocity; // [pixels per second] - private int mInitialTouchX; - private int mInitialTouchY; - private long mCheckingTouchSlop = RecyclerView.NO_ID; - - private ItemSlidingAnimator mItemSlideAnimator; - private SwipeableItemWrapperAdapter mAdapter; - private RecyclerView.ViewHolder mSwipingItem; - private Rect mSwipingItemMargins = new Rect(); - private int mTouchedItemOffsetX; - private int mLastTouchX; - private int mSwipingItemReactionType; - private VelocityTracker mVelocityTracker; - private SwipingItemOperator mSwipingItemOperator; - - /** - * Constructor. - */ - public RecyclerViewSwipeManager() { - mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - return RecyclerViewSwipeManager.this.onInterceptTouchEvent(rv, e); - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - RecyclerViewSwipeManager.this.onTouchEvent(rv, e); - } - }; - mItemSlideAnimator = new ItemSlidingAnimator(); - mVelocityTracker = VelocityTracker.obtain(); - } - - /** - * Create wrapped adapter. - * - * @param adapter The target adapter. - * - * @return Wrapped adapter which is associated to this {@link RecyclerViewSwipeManager} instance. - */ - @SuppressWarnings("unchecked") - public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) { - if (mAdapter != null) { - throw new IllegalStateException("already have a wrapped adapter"); - } - - mAdapter = new SwipeableItemWrapperAdapter(this, adapter); - - return mAdapter; - } - - /** - * 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. - * - * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set - * 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 - */ - public void attachRecyclerView(RecyclerView rv) { - if (rv == null) { - throw new IllegalArgumentException("RecyclerView cannot be null"); - } - - if (isReleased()) { - throw new IllegalStateException("Accessing released object"); - } - - if (mRecyclerView != null) { - throw new IllegalStateException("RecyclerView instance has already been set"); - } - - if (mAdapter == null || getSwipeableItemWrapperAdapter(rv) != mAdapter) { - throw new IllegalStateException("adapter is not set properly"); - } - - mRecyclerView = rv; - mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener); - - final ViewConfiguration vc = ViewConfiguration.get(rv.getContext()); - - mTouchSlop = vc.getScaledTouchSlop(); - mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); - mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); - - mItemSlideAnimator.setImmediatelySetTranslationThreshold( - (int) (rv.getResources().getDisplayMetrics().density * SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP + 0.5f)); - } - - /** - * 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); - } - mInternalUseOnItemTouchListener = null; - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - if (mItemSlideAnimator != null) { - mItemSlideAnimator.endAnimations(); - mItemSlideAnimator = null; - } - - mAdapter = null; - mRecyclerView = null; - } - - /** - * Indicates whether currently performing item swiping. - * - * @return True if currently performing item swiping - */ - public boolean isSwiping() { - return (mSwipingItem != null); - } - - /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onInterceptTouchEvent() action = " + action); - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (isSwiping()) { - handleActionUpOrCancelWhileSwiping(e); - return true; - } else { - handleActionUpOrCancelWhileNotSwiping(); - } - break; - - case MotionEvent.ACTION_DOWN: - if (!isSwiping()) { - handleActionDown(rv, e); - } - break; - - case MotionEvent.ACTION_MOVE: - if (isSwiping()) { - // NOTE: The first ACTION_MOVE event will come here. (maybe a bug of RecyclerView?) - handleActionMoveWhileSwiping(e); - return true; - } else { - if (handleActionMoveWhileNotSwiping(rv, e)) { - return true; - } - } - break; - } - - return false; - } - - /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) { - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onTouchEvent() action = " + action); - } - - if (!isSwiping()) { - // Log.w(TAG, "onTouchEvent() - unexpected state"); - return; - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - handleActionUpOrCancelWhileSwiping(e); - break; - - case MotionEvent.ACTION_MOVE: - handleActionMoveWhileSwiping(e); - break; - } - } - - private boolean handleActionDown(RecyclerView rv, MotionEvent e) { - final RecyclerView.Adapter adapter = rv.getAdapter(); - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY()); - - if (!(holder instanceof SwipeableItemViewHolder)) { - return false; - } - - final int itemPosition = holder.getPosition(); - - // verify the touched item is valid state - if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) { - return false; - } - if (holder.getItemId() != adapter.getItemId(itemPosition)) { - return false; - } - - final int touchX = (int) (e.getX() + 0.5f); - final int touchY = (int) (e.getY() + 0.5f); - - 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); - - final int reactionType = mAdapter.getSwipeReactionType(holder, viewX, viewY); - - if (reactionType == 0) { - return false; - } - - mInitialTouchX = touchX; - mInitialTouchY = touchY; - mCheckingTouchSlop = holder.getItemId(); - mSwipingItemReactionType = reactionType; - - return true; - } - - private static SwipeableItemWrapperAdapter getSwipeableItemWrapperAdapter(RecyclerView rv) { - return WrapperAdapterUtils.findWrappedAdapter(rv.getAdapter(), SwipeableItemWrapperAdapter.class); - } - - private void handleActionUpOrCancelWhileNotSwiping() { - mCheckingTouchSlop = RecyclerView.NO_ID; - mSwipingItemReactionType = 0; - } - - private boolean handleActionUpOrCancelWhileSwiping(MotionEvent e) { - mLastTouchX = (int) (e.getX() + 0.5f); - - int result = RESULT_CANCELED; - - if (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP) { - final int viewWidth = mSwipingItem.itemView.getWidth(); - final float distance = mLastTouchX - mInitialTouchX; - final float absDistance = Math.abs(distance); - - mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); // 1000: pixels per second - - final float xVelocity = mVelocityTracker.getXVelocity(); - final float absXVelocity = Math.abs(xVelocity); - - if ((absDistance > (mTouchSlop * MIN_DISTANCE_TOUCH_SLOP_MUL)) && - ((distance * xVelocity) > 0.0f) && - (absXVelocity <= mMaxFlingVelocity) && - ((absDistance > (viewWidth / 2)) || (absXVelocity >= mMinFlingVelocity))) { - - if ((distance < 0) && SwipeReactionUtils.canSwipeLeft(mSwipingItemReactionType)) { - result = RESULT_SWIPED_LEFT; - } else if ((distance > 0) && SwipeReactionUtils.canSwipeRight(mSwipingItemReactionType)) { - result = RESULT_SWIPED_RIGHT; - } - } - } - - if (LOCAL_LOGD) { - Log.d(TAG, "swping finished --- result = " + result); - } - - finishSwiping(result); - - return true; - } - - private boolean handleActionMoveWhileNotSwiping(RecyclerView rv, MotionEvent e) { - if (mCheckingTouchSlop == RecyclerView.NO_ID) { - return false; - } - - if (Math.abs(e.getY() - mInitialTouchY) > mTouchSlop) { - // scrolling occurred - mCheckingTouchSlop = RecyclerView.NO_ID; - return false; - } - - if (Math.abs(e.getX() - mInitialTouchX) <= mTouchSlop) { - return false; - } - - final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY()); - - if (holder == null || holder.getItemId() != mCheckingTouchSlop) { - mCheckingTouchSlop = RecyclerView.NO_ID; - return false; - } - - if (LOCAL_LOGD) { - Log.d(TAG, "swiping started"); - } - - startSwiping(rv, e, holder); - - return true; - } - - private void handleActionMoveWhileSwiping(MotionEvent e) { - mLastTouchX = (int) (e.getX() + 0.5f); - mVelocityTracker.addMovement(e); - - final int swipeDistance = mLastTouchX - mTouchedItemOffsetX; - - mSwipingItemOperator.update(swipeDistance); - } - - private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder) { - mSwipingItem = holder; - mLastTouchX = (int) (e.getX() + 0.5f); - mTouchedItemOffsetX = mLastTouchX; - CustomRecyclerViewUtils.getLayoutMargins(holder.itemView, mSwipingItemMargins); - - mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemReactionType); - mSwipingItemOperator.start(); - - mVelocityTracker.clear(); - mVelocityTracker.addMovement(e); - - mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); - - // raise onSwipeItemStarted() event - mAdapter.onSwipeItemStarted(this, holder); - } - - private void finishSwiping(int result) { - final RecyclerView.ViewHolder swipingItem = mSwipingItem; - - if (swipingItem == null) { - return; - } - - if (mRecyclerView != null && mRecyclerView.getParent() != null) { - mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); - } - - mVelocityTracker.clear(); - - mSwipingItem = null; - mLastTouchX = 0; - mInitialTouchX = 0; - mTouchedItemOffsetX = 0; - mCheckingTouchSlop = RecyclerView.NO_ID; - mSwipingItemReactionType = 0; - - if (mSwipingItemOperator != null) { - mSwipingItemOperator.finish(); - mSwipingItemOperator = null; - } - - final boolean toLeft = (result == RESULT_SWIPED_LEFT); - int afterReaction = AFTER_SWIPE_REACTION_DEFAULT; - - if (mAdapter != null) { - afterReaction = mAdapter.onSwipeItemFinished(swipingItem, result); - } - - afterReaction = correctAfterReaction(result, afterReaction); - - switch (afterReaction) { - case AFTER_SWIPE_REACTION_DEFAULT: - mItemSlideAnimator.slideToDefaultPosition(swipingItem, true, mReturnToDefaultPositionAnimationDuration); - break; - case AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION: - mItemSlideAnimator.slideToOutsideOfWindow( - swipingItem, toLeft, true, mMoveToOutsideWindowAnimationDuration); - break; - case AFTER_SWIPE_REACTION_REMOVE_ITEM: { - final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); - - final long removeAnimationDuration = (itemAnimator != null) ? itemAnimator.getRemoveDuration() : 0; - - if (supportsViewPropertyAnimator()) { - final long moveAnimationDuration = (itemAnimator != null) ? itemAnimator.getMoveDuration() : 0; - - final RemovingItemDecorator decorator = new RemovingItemDecorator( - mRecyclerView, swipingItem, removeAnimationDuration, moveAnimationDuration); - - decorator.setMoveAnimationInterpolator(SwipeDismissItemAnimator.MOVE_INTERPOLATOR); - decorator.start(); - } - - mItemSlideAnimator.slideToOutsideOfWindow( - swipingItem, toLeft, true, removeAnimationDuration); - } - break; - default: - throw new IllegalStateException("Unknwon after reaction type: " + afterReaction); - } - - if (mAdapter != null) { - mAdapter.onSwipeItemFinished2(swipingItem, result, afterReaction); - } - } - - private static int correctAfterReaction(int result, int afterReaction) { - if ((afterReaction == AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION) || - (afterReaction == AFTER_SWIPE_REACTION_REMOVE_ITEM)) { - if (!((result == RESULT_SWIPED_LEFT) || (result == RESULT_SWIPED_RIGHT))) { - afterReaction = AFTER_SWIPE_REACTION_DEFAULT; - } - } - - return afterReaction; - } - - public void cancelSwipe() { - finishSwiping(RESULT_CANCELED); - } - - /*package*/ boolean isAnimationRunning(RecyclerView.ViewHolder item) { - return (mItemSlideAnimator != null) && (mItemSlideAnimator.isRunning(item)); - } - - private void slideItem(RecyclerView.ViewHolder holder, float amount, boolean shouldAnimate) { - if (amount == OUTSIDE_OF_THE_WINDOW_LEFT) { - mItemSlideAnimator.slideToOutsideOfWindow(holder, true, shouldAnimate, mMoveToOutsideWindowAnimationDuration); - } else if (amount == OUTSIDE_OF_THE_WINDOW_RIGHT) { - mItemSlideAnimator.slideToOutsideOfWindow(holder, false, shouldAnimate, mMoveToOutsideWindowAnimationDuration); - } else if (amount == 0.0f) { - mItemSlideAnimator.slideToDefaultPosition(holder, shouldAnimate, mReturnToDefaultPositionAnimationDuration); - } else { - mItemSlideAnimator.slideToSpecifiedPosition(holder, amount); - } - } - - /** - * Gets the duration of the "return to default position" animation - * - * @return Duration of the "return to default position" animation in milliseconds - */ - public long getReturnToDefaultPositionAnimationDuration() { - return mReturnToDefaultPositionAnimationDuration; - } - - /** - * Sets the duration of the "return to default position" animation - * - * @param duration Duration of the "return to default position" animation in milliseconds - */ - public void setReturnToDefaultPositionAnimationDuration(long duration) { - mReturnToDefaultPositionAnimationDuration = duration; - } - - /** - * Gets the duration of the "move to outside of the window" animation - * - * @return Duration of the "move to outside of the window" animation in milliseconds - */ - public long getMoveToOutsideWindowAnimationDuration() { - return mMoveToOutsideWindowAnimationDuration; - } - - /** - * Sets the duration of the "move to outside of the window" animation - * - * @param duration Duration of the "move to outside of the window" animation in milliseconds - */ - public void setMoveToOutsideWindowAnimationDuration(long duration) { - mMoveToOutsideWindowAnimationDuration = duration; - } - - /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, float prevAmount, float amount, boolean shouldAnimate) { - final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder; - final View containerView = holder2.getSwipeableContainerView(); - - if (containerView == null) { - return; - } - - final int reqBackgroundType; - - if (amount == 0.0f) { - if (prevAmount == 0.0f) { - reqBackgroundType = DRAWABLE_SWIPE_NEUTRAL_BACKGROUND; - } else { - reqBackgroundType = (prevAmount < 0) - ? DRAWABLE_SWIPE_LEFT_BACKGROUND - : DRAWABLE_SWIPE_RIGHT_BACKGROUND; - } - } else { - reqBackgroundType = (amount < 0) - ? DRAWABLE_SWIPE_LEFT_BACKGROUND - : DRAWABLE_SWIPE_RIGHT_BACKGROUND; - } - - if (amount == 0.0f) { - slideItem(holder, amount, shouldAnimate); - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); - } else { - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); - slideItem(holder, amount, shouldAnimate); - } - } - - 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 deleted file mode 100644 index c79dc333f20..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * 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.swipeable; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.animation.Interpolator; - -import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils; - -import java.lang.ref.WeakReference; - -class RemovingItemDecorator extends RecyclerView.ItemDecoration { - @SuppressWarnings("unused") - private static final String TAG = "RemovingItemDecorator"; - - private static final int NOTIFY_REMOVAL_EFFECT_PHASE_1 = 0; - private static final int NOTIFY_REMOVAL_EFFECT_END = 1; - - private static final long ADDITIONAL_REMOVE_DURATION = 100; // workaround: to avoid the gap between the below item - - private RecyclerView mRecyclerView; - private RecyclerView.ViewHolder mSwipingItem; - private long mSwipingItemId; - private Rect mSwipingItemBounds = new Rect(); - private int mInitialTranslationX; - private int mTranslationY; - private long mStartTime; - private long mRemoveAnimationDuration; - private long mMoveAnimationDuration; - private Interpolator mMoveAnimationInterpolator; - private Drawable mSwipeBackgroundDrawable; - - private int mPendingNotificationMask = 0; - - public RemovingItemDecorator(RecyclerView rv, RecyclerView.ViewHolder swipingItem, long removeAnimationDuration, long moveAnimationDuration) { - mRecyclerView = rv; - mSwipingItem = swipingItem; - mSwipingItemId = swipingItem.getItemId(); - - mRemoveAnimationDuration = removeAnimationDuration + ADDITIONAL_REMOVE_DURATION; - mMoveAnimationDuration = moveAnimationDuration; - mInitialTranslationX = (int) (ViewCompat.getTranslationX(swipingItem.itemView) + 0.5f); - - CustomRecyclerViewUtils.getViewBounds(mSwipingItem.itemView, mSwipingItemBounds); - } - - public void setMoveAnimationInterpolator(Interpolator interpolator) { - mMoveAnimationInterpolator = interpolator; - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - final long elapsedTime = getElapsedTime(mStartTime); - - final Rect bounds = mSwipingItemBounds; - final float heightScale = determineBackgroundHeightScaleSwipeCompletedSuccessfully(elapsedTime); - - final int origHeight = bounds.height(); - final int actualHeight = (int) (heightScale * origHeight + 0.5f); - - fillSwipingItemBackground(c, mSwipeBackgroundDrawable, actualHeight); - - if (mSwipingItemId == mSwipingItem.getItemId()) { - mTranslationY = (int) (ViewCompat.getTranslationY(mSwipingItem.itemView) + 0.5f); - } - - if (requiresContinuousAnimationAfterSwipeCompletedSuccessfully(elapsedTime)) { - postInvalidateOnAnimation(); - } - } - - private boolean requiresContinuousAnimationAfterSwipeCompletedSuccessfully(long elapsedTime) { - return (elapsedTime >= mRemoveAnimationDuration) && - (elapsedTime < (mRemoveAnimationDuration + mMoveAnimationDuration)); - } - - private float determineBackgroundHeightScaleSwipeCompletedSuccessfully(long elapsedTime) { - float heightScale = 0.0f; - - if (elapsedTime < mRemoveAnimationDuration) { - heightScale = 1.0f; - } else if (elapsedTime < (mRemoveAnimationDuration + mMoveAnimationDuration)) { - if (mMoveAnimationDuration != 0) { - heightScale = 1.0f - (float) (elapsedTime - mRemoveAnimationDuration) / mMoveAnimationDuration; - if (mMoveAnimationInterpolator != null) { - heightScale = mMoveAnimationInterpolator.getInterpolation(heightScale); - } - } - } - - return heightScale; - } - - private void fillSwipingItemBackground(Canvas c, Drawable drawable, int height) { - final Rect bounds = mSwipingItemBounds; - final int translationX = mInitialTranslationX; - final int translationY = mTranslationY; - - if (height < 0) { - height = bounds.height(); - } - - if ((height == 0) || (drawable == null)) { - return; - } - - final int savedCount = c.save(); - - c.clipRect( - bounds.left + translationX, - bounds.top + translationY, - bounds.right + translationX, - bounds.top + translationY + height); - - // c.drawColor(0xffff0000); // <-- debug - - c.translate( - bounds.left + translationX, - bounds.top + translationY - (bounds.height() - height) / 2); - drawable.setBounds(0, 0, bounds.width(), bounds.height()); - - drawable.draw(c); - - c.restoreToCount(savedCount); - } - - private void postInvalidateOnAnimation() { -// final Rect bounds = mSwipingItemBounds; -// final int translationX = mInitialTranslationX; -// final int translationY = mTranslationY; - -// ViewCompat.postInvalidateOnAnimation( -// mRecyclerView, -// bounds.left + translationX, -// bounds.top + translationY, -// bounds.right + translationX, -// bounds.bottom + translationY); - ViewCompat.postInvalidateOnAnimation(mRecyclerView); - } - - public void start() { - final View containerView = ((SwipeableItemViewHolder) mSwipingItem).getSwipeableContainerView(); - - ViewCompat.animate(containerView).cancel(); - - mRecyclerView.addItemDecoration(this); - - mStartTime = System.currentTimeMillis(); - mTranslationY = (int) (ViewCompat.getTranslationY(mSwipingItem.itemView) + 0.5f); - mSwipeBackgroundDrawable = mSwipingItem.itemView.getBackground(); - - postInvalidateOnAnimation(); - notifyDelayed(NOTIFY_REMOVAL_EFFECT_PHASE_1, mRemoveAnimationDuration); - } - - private void notifyDelayed(int code, long delay) { - final int mask = (1 << code); - - if ((mPendingNotificationMask & mask) != 0) { - return; - } - - mPendingNotificationMask |= mask; - - final DelayedNotificationRunner notification = new DelayedNotificationRunner(this, code); - ViewCompat.postOnAnimationDelayed(mRecyclerView, notification, delay); - } - - /*package*/ void onDelayedNotification(int code) { - final int mask = (1 << code); - final long elapsedTime = getElapsedTime(mStartTime); - - mPendingNotificationMask &= (~mask); - - switch (code) { - case NOTIFY_REMOVAL_EFFECT_PHASE_1: - if (elapsedTime < mRemoveAnimationDuration) { - notifyDelayed(NOTIFY_REMOVAL_EFFECT_PHASE_1, (mRemoveAnimationDuration - elapsedTime)); - } else { - postInvalidateOnAnimation(); - notifyDelayed(NOTIFY_REMOVAL_EFFECT_END, mMoveAnimationDuration); - } - break; - case NOTIFY_REMOVAL_EFFECT_END: - finish(); - break; - } - } - - private void finish() { - mRecyclerView.removeItemDecoration(this); - postInvalidateOnAnimation(); // this is required to avoid remnant of the decoration - - mRecyclerView = null; - mSwipingItem = null; - mTranslationY = 0; - mMoveAnimationInterpolator = null; - } - - protected static long getElapsedTime(long initialTime) { - final long curTime = System.currentTimeMillis(); - return (curTime >= initialTime) ? (curTime - initialTime) : Long.MAX_VALUE; - } - - private static class DelayedNotificationRunner implements Runnable { - private WeakReference mRefDecorator; - private final int mCode; - - public DelayedNotificationRunner(RemovingItemDecorator decorator, int code) { - mRefDecorator = new WeakReference<>(decorator); - mCode = code; - } - - @Override - public void run() { - final RemovingItemDecorator decorator = mRefDecorator.get(); - mRefDecorator.clear(); - mRefDecorator = null; - - if (decorator != null) { - decorator.onDelayedNotification(mCode); - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 0c97a1afdcf..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.swipeable; - -import android.view.animation.Interpolator; - -class RubberBandInterpolator implements Interpolator { - private final float mLimit; - - public RubberBandInterpolator(float limit) { - mLimit = limit; - } - - @Override - public float getInterpolation(float input) { - final float t = (1 - input); - return mLimit * (1 - (t * t)); - } -} 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 deleted file mode 100644 index 5caa0c5aef9..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.swipeable; - -class SwipeReactionUtils { - public static final int REACTION_CAN_NOT_SWIPE = 0; - public static final int REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT = 1; - public static final int REACTION_CAN_SWIPE = 2; - - public static int extractLeftReaction(int type) { - return (type & 0x3); - } - - public static int extractRightReaction(int type) { - return ((type >>> 16) & 0x3); - } - - public static boolean canSwipeLeft(int reactionType) { - return (extractLeftReaction(reactionType) == REACTION_CAN_SWIPE); - } - - public static boolean canSwipeRight(int reactionType) { - return (extractRightReaction(reactionType) == REACTION_CAN_SWIPE); - } -} 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 deleted file mode 100644 index 8065322bf76..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.swipeable; - -import android.support.v7.widget.RecyclerView; - -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 x Touched X position. Relative from the itemView's top-left. - * @param y Touched Y position. Relative from the itemView's top-left. - - * @return Reaction type. Bitwise OR of these flags; - * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT} - * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT} - * - {@link RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT} - * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT} - * - {@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); - - /** - * Called when sets background of the swiping item. - * - * @param holder The ViewHolder which is associated to the swiping item. - * @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); - - /** - * Called when item is swiped. - * - * *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 result The result code of user's swipe operation. - * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * - * @return Reaction type of after swiping. - * One of the {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or - * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. - */ - int onSwipeItem(T holder, int result); - - /** - * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, 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 result The result code of user's swipe operation. - * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, - * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or - * {@link RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * @param reaction Reaction type. One of the {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}, - * {@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); -} 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 deleted file mode 100644 index 5c4714364dc..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.swipeable; - -import android.view.View; - -/** - * Interface which provides required information for swiping item. - * - * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}. - */ -public interface SwipeableItemViewHolder { - /** - * Sets the state flags value for swiping item - * - * @param flags Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_SWIPING} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_ACTIVE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_UPDATED} - */ - void setSwipeStateFlags(int flags); - - /** - * Gets the state flags value for swiping item - * - * @return Bitwise OR of these flags; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_SWIPING} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_ACTIVE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_UPDATED} - */ - int getSwipeStateFlags(); - - /** - * Sets the result code of swiping item. - * - * @param result Result code. One of these values; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_NONE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED} - */ - void setSwipeResult(int result); - - /** - * Gets the result code of swiping item. - * - * @return Result code. One of these values; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_NONE} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED} - */ - int getSwipeResult(); - - /** - * Sets the reaction type of after swiping item. - * - * @param reaction After-reaction type. One of these values; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} - */ - void setAfterSwipeReaction(int reaction); - - /** - * Gets the reaction type of after swiping item. - * - * @return After-reaction type. One of these values; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} - */ - int getAfterSwipeReaction(); - - /** - * Sets the item swipe amount. - * - * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0], but these special values can be accepted; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_RIGHT} - */ - void setSwipeItemSlideAmount(float amount); - - /** - * Gets the item swipe amount. - * - * @return Item swipe amount. Generally the range is [-1.0 .. 1.0], but these special values may be returned; - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_LEFT} - * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_RIGHT} - */ - float getSwipeItemSlideAmount(); - - /** - * Gets the container view for the swipeable area. - * - * @return The container view instance. - */ - View getSwipeableContainerView(); -} 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 deleted file mode 100644 index 6cc6ee9607a..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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.swipeable; - -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter; -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; - -class SwipeableItemWrapperAdapter extends BaseWrapperAdapter { - private static final String TAG = "ARVSwipeableWrapper"; - - private static final int STATE_FLAG_INITIAL_VALUE = -1; - - private static final boolean LOCAL_LOGV = false; - private static final boolean LOCAL_LOGD = false; - - private SwipeableItemAdapter mSwipeableItemAdapter; - private RecyclerViewSwipeManager mSwipeManager; - private int mSwipingItemPosition = RecyclerView.NO_POSITION; - - public SwipeableItemWrapperAdapter(RecyclerViewSwipeManager manager, RecyclerView.Adapter adapter) { - super(adapter); - - mSwipeableItemAdapter = getSwipeableItemAdapter(adapter); - if (mSwipeableItemAdapter == null) { - throw new IllegalArgumentException("adapter does not implement SwipeableItemAdapter"); - } - - if (manager == null) { - throw new IllegalArgumentException("manager cannot be null"); - } - - mSwipeManager = manager; - } - - @Override - protected void onRelease() { - super.onRelease(); - - mSwipeableItemAdapter = null; - mSwipeManager = null; - mSwipingItemPosition = RecyclerView.NO_POSITION; - } - - @Override - public void onViewRecycled(VH holder) { - super.onViewRecycled(holder); - - // reset SwipeableItemViewHolder state - if (holder instanceof SwipeableItemViewHolder) { - ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(0.0f); - - View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); - - if (containerView != null) { - ViewCompat.animate(containerView).cancel(); - ViewCompat.setTranslationX(containerView, 0.0f); - } - } - } - - @Override - public VH onCreateViewHolder(ViewGroup parent, int viewType) { - final VH holder = super.onCreateViewHolder(parent, viewType); - - if (holder instanceof SwipeableItemViewHolder) { - ((SwipeableItemViewHolder) holder).setSwipeStateFlags(STATE_FLAG_INITIAL_VALUE); - } - - return holder; - } - - @Override - public void onBindViewHolder(VH holder, int position) { - float prevSwipeItemSlideAmount = 0; - - if (holder instanceof SwipeableItemViewHolder) { - prevSwipeItemSlideAmount = ((SwipeableItemViewHolder) holder).getSwipeItemSlideAmount(); - } - - if (isSwiping()) { - int flags = RecyclerViewSwipeManager.STATE_FLAG_SWIPING; - - if (position == mSwipingItemPosition) { - flags |= RecyclerViewSwipeManager.STATE_FLAG_IS_ACTIVE; - } - - safeUpdateFlags(holder, flags); - super.onBindViewHolder(holder, position); - } else { - safeUpdateFlags(holder, 0); - super.onBindViewHolder(holder, position); - } - - if (holder instanceof SwipeableItemViewHolder) { - final float swipeItemSlideAmount = ((SwipeableItemViewHolder) holder).getSwipeItemSlideAmount(); - - if ((prevSwipeItemSlideAmount != swipeItemSlideAmount) || - !(mSwipeManager.isSwiping() || mSwipeManager.isAnimationRunning(holder))) { - mSwipeManager.applySlideItem(holder, prevSwipeItemSlideAmount, swipeItemSlideAmount, true); - } - } - } - - @Override - protected void onHandleWrappedAdapterChanged() { - if (isSwiping()) { - cancelSwipe(); - } else { - super.onHandleWrappedAdapterChanged(); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) { - if (isSwiping()) { - cancelSwipe(); - } else { - super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) { - if (isSwiping()) { - cancelSwipe(); - } else { - super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) { - if (isSwiping()) { - cancelSwipe(); - } else { - super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount); - } - } - - @Override - protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) { - if (isSwiping()) { - cancelSwipe(); - } else { - super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount); - } - } - - private void cancelSwipe() { - if (mSwipeManager != null) { - mSwipeManager.cancelSwipe(); - } - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ - @SuppressWarnings("unchecked") - int getSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) { - if (LOCAL_LOGV) { - Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", x = " + x + ", y = " + y + ")"); - } - - return mSwipeableItemAdapter.onGetSwipeReactionType(holder, x, y); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ - @SuppressWarnings("unchecked") - void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int type) { - if (LOCAL_LOGV) { - Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", type = " + type + ")"); - } - - mSwipeableItemAdapter.onSetSwipeBackground(holder, type); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder) { - if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ")"); - } - - mSwipingItemPosition = holder.getPosition(); - - notifyDataSetChanged(); - } - - // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ - @SuppressWarnings("unchecked") - int onSwipeItemFinished(RecyclerView.ViewHolder holder, int result) { - if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", result = " + result + ")"); - } - - mSwipingItemPosition = RecyclerView.NO_POSITION; - - return mSwipeableItemAdapter.onSwipeItem(holder, result); - } - - /*package*/ - @SuppressWarnings("unchecked") - void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int result, int afterReaction) { - - ((SwipeableItemViewHolder) holder).setSwipeResult(result); - ((SwipeableItemViewHolder) holder).setAfterSwipeReaction(afterReaction); - ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(getSwipeAmountFromAfterReaction(result, afterReaction)); - - mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, result, afterReaction); - notifyDataSetChanged(); - } - - protected boolean isSwiping() { - return (mSwipingItemPosition != RecyclerView.NO_POSITION); - } - - private static float getSwipeAmountFromAfterReaction(int result, int afterReaction) { - switch (afterReaction) { - case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT: - return 0.0f; - case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION: - case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_REMOVE_ITEM: - return (result == RecyclerViewSwipeManager.RESULT_SWIPED_LEFT) - ? RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_LEFT - : RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_RIGHT; - default: - return 0.0f; - } - } - - private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) { - if (!(holder instanceof SwipeableItemViewHolder)) { - return; - } - - final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder; - - final int curFlags = holder2.getSwipeStateFlags(); - final int mask = ~RecyclerViewSwipeManager.STATE_FLAG_IS_UPDATED; - - // append UPDATED flag - if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) { - flags |= RecyclerViewSwipeManager.STATE_FLAG_IS_UPDATED; - } - - ((SwipeableItemViewHolder) holder).setSwipeStateFlags(flags); - } - - private static SwipeableItemAdapter getSwipeableItemAdapter(RecyclerView.Adapter adapter) { - return WrapperAdapterUtils.findWrappedAdapter(adapter, SwipeableItemAdapter.class); - } -} 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 deleted file mode 100644 index c735d436266..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.swipeable; - -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.animation.Interpolator; - -class SwipingItemOperator { - @SuppressWarnings("unused") - private static final String TAG = "SwipingItemOperator"; - - private static final int REACTION_CAN_NOT_SWIPE = SwipeReactionUtils.REACTION_CAN_NOT_SWIPE; - private static final int REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT = SwipeReactionUtils.REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT; - private static final int REACTION_CAN_SWIPE = SwipeReactionUtils.REACTION_CAN_SWIPE; - - private static final float RUBBER_BAND_LIMIT = 0.15f; - - private static final Interpolator RUBBER_BAND_INTERPOLATOR = new RubberBandInterpolator(RUBBER_BAND_LIMIT); - - private RecyclerViewSwipeManager mSwipeManager; - private RecyclerView.ViewHolder mSwipingItem; - private View mSwipingItemContainerView; - private int mLeftSwipeReactionType; - private int mRightSwipeReactionType; - private int mSwipingItemWidth; - private float mInvSwipingItemWidth; - private int mSwipeDistance; - private float mPrevTranslateAmount; - - public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int swipeReactionType) { - mSwipeManager = manager; - mSwipingItem = swipingItem; - mLeftSwipeReactionType = SwipeReactionUtils.extractLeftReaction(swipeReactionType); - mRightSwipeReactionType = SwipeReactionUtils.extractRightReaction(swipeReactionType); - - mSwipingItemContainerView = ((SwipeableItemViewHolder) swipingItem).getSwipeableContainerView(); - mSwipingItemWidth = mSwipingItemContainerView.getWidth(); - mInvSwipingItemWidth = (mSwipingItemWidth != 0) ? (1.0f / mSwipingItemWidth) : 0.0f; - } - - @SuppressWarnings("EmptyMethod") - public void start() { - // nothing to do here. - } - - public void finish() { - mSwipeManager = null; - mSwipingItem = null; - mSwipeDistance = 0; - mSwipingItemWidth = 0; - mInvSwipingItemWidth = 0; - mLeftSwipeReactionType = REACTION_CAN_NOT_SWIPE; - mRightSwipeReactionType = REACTION_CAN_NOT_SWIPE; - mPrevTranslateAmount = 0; - mSwipingItemContainerView = null; - } - - public void update(int swipeDistance) { - if (mSwipeDistance == swipeDistance) { - return; - } - - mSwipeDistance = swipeDistance; - - final int reactionType = (mSwipeDistance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType; - - float translateAmount = 0; - - switch (reactionType) { - 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); - break; - case REACTION_CAN_SWIPE: - translateAmount = Math.min(Math.max((mSwipeDistance * mInvSwipingItemWidth), -1.0f), 1.0f); - break; - } - - mSwipeManager.applySlideItem(mSwipingItem, mPrevTranslateAmount, translateAmount, false); - - mPrevTranslateAmount = translateAmount; - } -} \ No newline at end of file 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 deleted file mode 100644 index 3f8003af242..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.touchguard; - -import android.support.v4.view.MotionEventCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.MotionEvent; -import android.view.ViewConfiguration; - -public class RecyclerViewTouchActionGuardManager { - private static final String TAG = "ARVTouchActionGuardMgr"; - - private static final boolean LOCAL_LOGV = false; - private static final boolean LOCAL_LOGD = false; - - private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; - private RecyclerView mRecyclerView; - private boolean mGuarding; - private int mInitialTouchY; - private int mLastTouchY; - private int mTouchSlop; - private boolean mEnabled; - private boolean mInterceptScrollingWhileAnimationRunning; - - public RecyclerViewTouchActionGuardManager() { - mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - return RecyclerViewTouchActionGuardManager.this.onInterceptTouchEvent(rv, e); - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - RecyclerViewTouchActionGuardManager.this.onTouchEvent(rv, e); - } - }; - } - - public boolean isReleased() { - return (mInternalUseOnItemTouchListener == null); - } - - public void attachRecyclerView(RecyclerView rv) { - if (rv == null) { - throw new IllegalArgumentException("RecyclerView cannot be null"); - } - - if (isReleased()) { - throw new IllegalStateException("Accessing released object"); - } - - if (mRecyclerView != null) { - throw new IllegalStateException("RecyclerView instance has already been set"); - } - - mRecyclerView = rv; - mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener); - - mTouchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop(); - } - - public void release() { - if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) { - mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); - } - mInternalUseOnItemTouchListener = null; - mRecyclerView = null; - } - - /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - if (!mEnabled) { - return false; - } - - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onInterceptTouchEvent() action = " + action); - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - handleActionUpOrCancel(); - break; - - case MotionEvent.ACTION_DOWN: - handleActionDown(e); - break; - - case MotionEvent.ACTION_MOVE: - if (handleActionMove(rv, e)) { - return true; - } - break; - } - - return false; - } - - /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) { - if (!mEnabled) { - return; - } - - final int action = MotionEventCompat.getActionMasked(e); - - if (LOCAL_LOGV) { - Log.v(TAG, "onTouchEvent() action = " + action); - } - - switch (action) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - handleActionUpOrCancel(); - break; - } - } - - private boolean handleActionMove(RecyclerView rv, MotionEvent e) { - if (!mGuarding) { - mLastTouchY = (int) (e.getY() + 0.5f); - - final int distance = mLastTouchY - mInitialTouchY; - - if (mInterceptScrollingWhileAnimationRunning && (Math.abs(distance) > mTouchSlop) && isAnimationRunning(rv)) { - // intercept vertical move touch events while animation is running - mGuarding = true; - } - } - - return mGuarding; - } - - private static boolean isAnimationRunning(RecyclerView rv) { - final RecyclerView.ItemAnimator itemAnimator = rv.getItemAnimator(); - return (itemAnimator != null) && (itemAnimator.isRunning()); - } - - private void handleActionUpOrCancel() { - mGuarding = false; - mInitialTouchY = 0; - mLastTouchY = 0; - } - - private void handleActionDown(MotionEvent e) { - mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); - mGuarding = false; - } - - public void setEnabled(boolean enabled) { - if (mEnabled == enabled) { - return; - } - mEnabled = enabled; - - if (!mEnabled) { - handleActionUpOrCancel(); - } - } - - public boolean isEnabled() { - return mEnabled; - } - - public void setInterceptVerticalScrollingWhileAnimationRunning(boolean value) { - mInterceptScrollingWhileAnimationRunning = value; - } - - 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 deleted file mode 100644 index 9cc64c00ae1..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder; - -public abstract class AbstractDraggableItemViewHolder extends RecyclerView.ViewHolder implements DraggableItemViewHolder { - private int mDragStateFlags; - - public AbstractDraggableItemViewHolder(View itemView) { - super(itemView); - } - - @Override - public void setDragStateFlags(int flags) { - mDragStateFlags = flags; - } - - @Override - public int getDragStateFlags() { - return mDragStateFlags; - } -} 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 deleted file mode 100644 index 3e8dc254a1a..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.utils; - -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 AbstractDraggableSwipeableItemViewHolder 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 AbstractDraggableSwipeableItemViewHolder(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 int getAfterSwipeReaction() { - return mAfterSwipeReaction; - } - - @Override - public void setAfterSwipeReaction(int reaction) { - mAfterSwipeReaction = reaction; - } - - @Override - public float getSwipeItemSlideAmount() { - return mSwipeAmount; - } - - @Override - public void setSwipeItemSlideAmount(float amount) { - mSwipeAmount = amount; - } - -} 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 deleted file mode 100644 index 6fb94598a9d..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; - -import com.h6ah4i.android.widget.advrecyclerview.expandable.ExpandableItemAdapter; - -public abstract class AbstractExpandableItemAdapter - extends RecyclerView.Adapter - implements ExpandableItemAdapter { - - /** - * This method will not be called. - * Override {@link #onCreateGroupViewHolder(android.view.ViewGroup, int)} and - * {@link #onCreateChildViewHolder(android.view.ViewGroup, int)} instead. - * - * @param parent not used - * @param viewType not used - * @return null - */ - @Override - public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return null; - } - - /** - * This method will not be called. - * Override {@link #getGroupId(int)} and {@link #getChildId(int, int)} instead. - * - * @param position not used - * @return {@link RecyclerView#NO_ID} - */ - @Override - public final long getItemId(int position) { - return RecyclerView.NO_ID; - } - - /** - * This method will not be called. - * Override {@link #getGroupItemViewType(int)} and {@link #getChildItemViewType(int, int)} instead. - * - * @param position not used - * @return 0 - */ - @Override - public final int getItemViewType(int position) { - return 0; - } - - /** - * This method will not be called. - * Override {@link #getGroupCount()} and {@link #getChildCount(int)} instead. - * - * @return 0 - */ - @Override - public final int getItemCount() { - return 0; - } - - /** - * This method will not be called. - * Override {@link #onBindGroupViewHolder(RecyclerView.ViewHolder, int, int)} ()} and - * {@link #onBindChildViewHolder(RecyclerView.ViewHolder, int, int, int)} instead. - * - * @param holder not used - * @param position not used - */ - @Override - public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - } -} \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java deleted file mode 100644 index a0dd61b5aae..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.expandable.ExpandableItemViewHolder; - -public abstract class AbstractExpandableItemViewHolder extends RecyclerView.ViewHolder implements ExpandableItemViewHolder { - private int mExpandStateFlags; - - public AbstractExpandableItemViewHolder(View itemView) { - super(itemView); - } - - @Override - public void setExpandStateFlags(int flags) { - mExpandStateFlags = flags; - } - - @Override - public int getExpandStateFlags() { - return mExpandStateFlags; - } -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java deleted file mode 100644 index f7a084eeb6f..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder; - -public abstract class AbstractSwipeableItemViewHolder extends RecyclerView.ViewHolder implements SwipeableItemViewHolder { - private int mSwipeStateFlags; - private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE; - private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; - private float mSwipeAmount; - - public AbstractSwipeableItemViewHolder(View itemView) { - super(itemView); - } - - @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 int getAfterSwipeReaction() { - return mAfterSwipeReaction; - } - - @Override - public void setAfterSwipeReaction(int reaction) { - mAfterSwipeReaction = reaction; - } - - @Override - public float getSwipeItemSlideAmount() { - return mSwipeAmount; - } - - @Override - public void setSwipeItemSlideAmount(float amount) { - mSwipeAmount = amount; - } - -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java deleted file mode 100644 index a82009ebadd..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.ViewGroup; - -import java.lang.ref.WeakReference; - -public class BaseWrapperAdapter - extends RecyclerView.Adapter { - private static final String TAG = "ARVBaseWrapperAdapter"; - private static final boolean LOCAL_LOGD = false; - - private RecyclerView.Adapter mWrappedAdapter; - private BridgeObserver mBridgeObserver; - - public BaseWrapperAdapter(RecyclerView.Adapter adapter) { - mWrappedAdapter = adapter; - mBridgeObserver = new BridgeObserver<>(this); - mWrappedAdapter.registerAdapterDataObserver(mBridgeObserver); - super.setHasStableIds(mWrappedAdapter.hasStableIds()); - } - - public void release() { - onRelease(); - - if (mWrappedAdapter != null && mBridgeObserver != null) { - mWrappedAdapter.unregisterAdapterDataObserver(mBridgeObserver); - } - - mWrappedAdapter = null; - mBridgeObserver = null; - } - - protected void onRelease() { - // override this method if needed - } - - public RecyclerView.Adapter getWrappedAdapter() { - return mWrappedAdapter; - } - - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - mWrappedAdapter.onAttachedToRecyclerView(recyclerView); - } - - @Override - public void onDetachedFromRecyclerView(RecyclerView recyclerView) { - mWrappedAdapter.onDetachedFromRecyclerView(recyclerView); - } - - @Override - public void onViewAttachedToWindow(VH holder) { - mWrappedAdapter.onViewAttachedToWindow(holder); - } - - @Override - public void onViewDetachedFromWindow(VH holder) { - mWrappedAdapter.onViewDetachedFromWindow(holder); - } - - @Override - public void onViewRecycled(VH holder) { - mWrappedAdapter.onViewRecycled(holder); - } - - @Override - public void setHasStableIds(boolean hasStableIds) { - super.setHasStableIds(hasStableIds); - mWrappedAdapter.setHasStableIds(hasStableIds); - } - - @Override - public VH onCreateViewHolder(ViewGroup parent, int viewType) { - return mWrappedAdapter.onCreateViewHolder(parent, viewType); - } - - @Override - public void onBindViewHolder(VH holder, int position) { - mWrappedAdapter.onBindViewHolder(holder, position); - } - - @Override - public int getItemCount() { - return mWrappedAdapter.getItemCount(); - } - - @Override - public long getItemId(int position) { - return mWrappedAdapter.getItemId(position); - } - - @Override - public int getItemViewType(int position) { - return mWrappedAdapter.getItemViewType(position); - } - - protected void onHandleWrappedAdapterChanged() { - notifyDataSetChanged(); - } - - protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) { - notifyItemRangeChanged(positionStart, itemCount); - } - - protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) { - notifyItemRangeInserted(positionStart, itemCount); - } - - protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) { - notifyItemRangeRemoved(positionStart, itemCount); - } - - protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) { - if (itemCount != 1) { - throw new IllegalStateException("itemCount should be always 1 (actual: " + itemCount + ")"); - } - - notifyItemMoved(fromPosition, toPosition); - } - - /*package*/ final void onWrappedAdapterChanged() { - if (LOCAL_LOGD) { - Log.d(TAG, "onWrappedAdapterChanged"); - } - - onHandleWrappedAdapterChanged(); - } - - /*package*/ final void onWrappedAdapterItemRangeChanged(int positionStart, int itemCount) { - if (LOCAL_LOGD) { - Log.d(TAG, "onWrappedAdapterItemRangeChanged(positionStart = " + positionStart + ", itemCount = " + itemCount + ")"); - } - - onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount); - } - - /*package*/ final void onWrappedAdapterItemRangeInserted(int positionStart, int itemCount) { - if (LOCAL_LOGD) { - Log.d(TAG, "onWrappedAdapterItemRangeInserted(positionStart = " + positionStart + ", itemCount = " + itemCount + ")"); - } - - onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount); - } - - /*package*/ final void onWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) { - if (LOCAL_LOGD) { - Log.d(TAG, "onWrappedAdapterItemRangeRemoved(positionStart = " + positionStart + ", itemCount = " + itemCount + ")"); - } - - onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount); - } - - /*package*/ final void onWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) { - if (LOCAL_LOGD) { - Log.d(TAG, "onWrappedAdapterRangeMoved(fromPosition = " + fromPosition + ", toPosition = " + toPosition + ", itemCount = " + itemCount + ")"); - } - - onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount); - } - - private static final class BridgeObserver extends RecyclerView.AdapterDataObserver { - private WeakReference> mRefHolder; - - public BridgeObserver(BaseWrapperAdapter holder) { - mRefHolder = new WeakReference<>(holder); - } - - @Override - public void onChanged() { - final BaseWrapperAdapter holder = mRefHolder.get(); - if (holder != null) { - holder.onWrappedAdapterChanged(); - } - } - - @Override - public void onItemRangeChanged(int positionStart, int itemCount) { - final BaseWrapperAdapter holder = mRefHolder.get(); - if (holder != null) { - holder.onWrappedAdapterItemRangeChanged(positionStart, itemCount); - } - } - - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - final BaseWrapperAdapter holder = mRefHolder.get(); - if (holder != null) { - holder.onWrappedAdapterItemRangeInserted(positionStart, itemCount); - } - } - - @Override - public void onItemRangeRemoved(int positionStart, int itemCount) { - final BaseWrapperAdapter holder = mRefHolder.get(); - if (holder != null) { - holder.onWrappedAdapterItemRangeRemoved(positionStart, itemCount); - } - } - - @Override - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - final BaseWrapperAdapter holder = mRefHolder.get(); - - if (holder != null) { - holder.onWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount); - } - } - } -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java deleted file mode 100644 index f5f96a04313..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.utils; - -import android.graphics.Rect; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.ViewGroup; - -public class CustomRecyclerViewUtils { - public static RecyclerView.ViewHolder findChildViewHolderUnderWithoutTranslation(RecyclerView rv, float x, float y) { - final View child = findChildViewUnderWithoutTranslation(rv, x, y); - return (child != null) ? rv.getChildViewHolder(child) : null; - } - - private static View findChildViewUnderWithoutTranslation(ViewGroup parent, float x, float y) { - final int count = parent.getChildCount(); - for (int i = count - 1; i >= 0; i--) { - final View child = parent.getChildAt(i); - if (x >= child.getLeft() && - x <= child.getRight() && - y >= child.getTop() && - y <= child.getBottom()) { - return child; - } - } - return null; - } - - public static RecyclerView.ViewHolder findChildViewHolderUnderWithTranslation(RecyclerView rv, float x, float y) { - final View child = rv.findChildViewUnder(x, y); - return (child != null) ? rv.getChildViewHolder(child) : null; - } - - public static Rect getLayoutMargins(View v, Rect outMargins) { - final ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); - if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - final ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; - outMargins.left = marginLayoutParams.leftMargin; - outMargins.right = marginLayoutParams.rightMargin; - outMargins.top = marginLayoutParams.topMargin; - outMargins.bottom = marginLayoutParams.bottomMargin; - } else { - outMargins.left = outMargins.right = outMargins.top = outMargins.bottom = 0; - } - return outMargins; - } - - public static Rect getDecorationOffsets(RecyclerView.LayoutManager layoutManager, View view, Rect outDecorations) { - outDecorations.left = layoutManager.getLeftDecorationWidth(view); - outDecorations.right = layoutManager.getRightDecorationWidth(view); - outDecorations.top = layoutManager.getTopDecorationHeight(view); - outDecorations.bottom = layoutManager.getBottomDecorationHeight(view); - - return outDecorations; - } - - public static Rect getViewBounds(View v, Rect outBounds) { - outBounds.left = v.getLeft(); - outBounds.right = v.getRight(); - outBounds.top = v.getTop(); - outBounds.bottom = v.getBottom(); - return outBounds; - } - - - public static int findFirstVisibleItemPosition(RecyclerView rv) { - RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); - - if (layoutManager instanceof LinearLayoutManager) { - return (((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition()); - } else { - return RecyclerView.NO_POSITION; - } - } - - public static int findLastVisibleItemPosition(RecyclerView rv) { - RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); - - if (layoutManager instanceof LinearLayoutManager) { - return (((LinearLayoutManager) layoutManager).findLastVisibleItemPosition()); - } else { - return RecyclerView.NO_POSITION; - } - } - - public static int findFirstCompletelyVisibleItemPosition(RecyclerView rv) { - RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); - - if (layoutManager instanceof LinearLayoutManager) { - return (((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition()); - } else { - return RecyclerView.NO_POSITION; - } - } - - public static int findLastCompletelyVisibleItemPosition(RecyclerView rv) { - RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); - - if (layoutManager instanceof LinearLayoutManager) { - return (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()); - } else { - return RecyclerView.NO_POSITION; - } - } -} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java deleted file mode 100644 index c13450cc8bd..00000000000 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.utils; - -import android.support.v7.widget.RecyclerView; - -public class WrapperAdapterUtils { - - private WrapperAdapterUtils() { - } - - public static T findWrappedAdapter(RecyclerView.Adapter adapter, Class clazz) { - if (clazz.isInstance(adapter)) { - return clazz.cast(adapter); - } else if (adapter instanceof BaseWrapperAdapter) { - final RecyclerView.Adapter wrappedAdapter = ((BaseWrapperAdapter) adapter).getWrappedAdapter(); - return findWrappedAdapter(wrappedAdapter, clazz); - } else { - return null; - } - } - - public static RecyclerView.Adapter releaseAll(RecyclerView.Adapter adapter) { - return releaseCyclically(adapter); - } - - private static RecyclerView.Adapter releaseCyclically(RecyclerView.Adapter adapter) { - if (!(adapter instanceof BaseWrapperAdapter)) { - return adapter; - } - - final BaseWrapperAdapter wrapperAdapter = (BaseWrapperAdapter) adapter; - final RecyclerView.Adapter wrappedAdapter = wrapperAdapter.getWrappedAdapter(); - - wrapperAdapter.release(); - - return releaseCyclically(wrappedAdapter); - } -} diff --git a/mobile/google-android-support-v13/.classpath b/mobile/google-android-support-v13/.classpath new file mode 100644 index 00000000000..7d0c3fd807c --- /dev/null +++ b/mobile/google-android-support-v13/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + 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..54e493c07c9 --- /dev/null +++ b/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +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.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +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/libs/internal_impl-22.2.1.jar b/mobile/google-android-support-v13/libs/internal_impl-22.2.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..f68c4ce4179a4bbf64ecc571e3f6983c1d3387be GIT binary patch literal 1708 zcmWIWW@h1HVBp|jP?7rR#sCCNAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM95_hy#O9 zACEq_qQvyv)Vvbs{M>@X5>Mw~z2uz4;^N)_U;jf60=3LHyw-kNDALmHe&EQlf+Gcr ziiVE12^Iyn4|RDj$i1c8X@9y~P3{llKaIzEidjnR<~QfgHUE5X&za-TZ(BF;Nr@F% zYz$~jUX-=S&0t-NeoFs@+-=8NV(e!WIwfk<)XiRFBX`$0f8~Tv1~WE2o#E>AJla5A z-qpuK-l$%THR;(gPYJe(CD*6RPH$ak-EpJlXzfk@FvZE&{{~JE=-RpGVV#lK#t%n= zPN+NW{i*wL_O-bg{$Csl%C2)hi#nZQY@!&kSfO)Sje^O8KQrq#CS<22^S5;dOPjrF zxwe0=aM!H&dNb|I-|u0a?{(;;UVN6ZZ>ntiiB+wyCkKZ2wi>OeTi3N@;y3LVmb&j( zU-vl3_371CbKVUF3zFiVwnrEFmu6cozFXn=tUY&WMrdKy%BIaP4+(OuWMy`pa_QjH zDaC(SQPb$tlSVb}z$j4$Mu`zAQQ{k92#J-c&{$Ch#!Ax(bMbx;9^Qu?51pJ$9DQ9Z z3!)Q_s$MY5eOO<4@kq1%0r}K9x-PZ_g_G~ztTcXq_W1Mbzu6naR*6kI5qV;9!lH~t zE(Us2S1;mI478RPniQL_#rbky(bKnCmeUS4OXy3S{I<6;Mu;LX&VFF`WN!FX@%&NEWOha zu=4MTKTEf{@6xYQ5-B^*sVSN_)uPa41KT~V87Jmla;OaF@7)=}E&bcTXG-pk6#{zy zj9C;HtIlaHNI!SCLQmbsF)zG+&r+%W24j_qpTFLGe0oY@#-HLFliXhAFPLsob6~w- zbLEmBTg^E)6r3X}JbsT3-JDVU zi#NcVkx7IZcPRsm8z2y1cioy5CGCO56Fb+f)y(Gbb|^Z2mt9m2V|n_1{F#8OaK*05CAfP37ERE znE)!F&`m)vpkU@KX|%#)3Xb9m-Ej1*hA{jrFuZX&5yNnJ#tZOfWdoVP0fa_C6H-_} FJOC<37-Rqd literal 0 HcmV?d00001 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..980b98c1d5f --- /dev/null +++ b/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +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.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +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/libs/internal_impl-22.2.1.jar b/mobile/google-android-support-v4/libs/internal_impl-22.2.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..33e6c5156b846489979cf3c35df085b551caeded GIT binary patch literal 266310 zcmbTe1yp277A=Z96z=Y>g}Zy=?(Xg`g=^t%g*z1P?(R^y749wtpPrtXH{CNm^WM+3 zf|V;XB5vNJd!INLd1+8E7$6`hC?Is~D>0yt7szjaK3>otLsnE-kWLaH#sCT=|A&d+ z{XoufJ|gf5ER6qf(Lc{a{Fix7F820zj?VveKFWWX@A~<_F2VTEOAPGo|7}?l zR)OLUFc1(n7!c5JLj1=a{NtB>TWNqN&RH!6thr)#DW1DQ95YiYFRIQl%Ss>6q$sZIMO(DSspiGNdmMKMd1}m?7-Gd@IdF)R5gql-Vf(f0qyBCEPj}bu*qJ zUB6DqPjM44xo1t;;Mz6GN=$6f!l0w#HBAHI3YH)(9@ihZOv>y9@#kp5g{!M(P|sXQ zaMHr1{r99e&~-!NR37QVcsI^DI2sjZlifba$p%itDu(qk#M~oJHrk(k@UM7c8ygaG zj8Df7PiPNp6SQWY+6Ge+9ckAP*a3sg2ct+fZL8dv_}5giidBO%RV<3p&@JxBBh$zC zfId3J^Q@ZW*kIn6XF}$)et~@yj2(O;DTEOCZ%;#lQq8lRQo2%0_t4k3=Hewm)R>eM z5yz_1_25h4HW5}&9NDi(i=+=kP$KCx#`X7a$@!_UXS;bU ziOyNt`7VMmll__=e;vyAEAM99e0Mc{VC)AC!FXTw4jD3|4%(j4Pz1M1&1YBjzR?$z zmR$U`Yjg{Wf$t0cA~cALF0m*H$_wTm0ih9Xgh+3QwAYWE+|e0Sw3K3Yf?nn61HaVK z$uvjPeAJ@W(Jm@8pc{P34Hbx8W*oeL{o`;;O8y1>|2Wle_AOW`Sy%dDMPQ&nKzx5~ zMT#aicFrb}w)QU0|6)Y{^6Br^lco5(^#D?=6CgCD=7eB^rFco9YD4ek(Ub+%M9S0y z=5z74_N>LiX@+#$H+)|!vZ?R7KW&K(cQBt_i;*H?6_m zQ3zuVXaP;vlzSnkX&12C#B{7p_5gT<4S^Ww6>YS#1UyTdByOUm9^E5=2QJ4*=<_Lr zY@p2%wS00cS*c51Rz4O!Qf}#Tgz)|57?35B9#?0Hx-Xinage`TDPgMNEGueVoObKQ zTpVq+fkA64^X7jo^wQMtncUj3CxJgEAfRHUdg_4gr%<|Vp@Sj7;@0w3i z;FarR{D>QKAt3_NkI05XhPe`30k(u-B0@qDqd?AXD4ehV1eq)|GS zV-FiyIaBxRQx{9@{;;Qz<|Nm(XGTLq^n$mJIKv*;3&X!E0_V(azT5$j(q#iX|hfj;mAqfy?g6RUf*-gHiLtr?h+q=?HvvuoafDjV()lzgJV8kTX+Jz2FhQSP1V^bCEyEdiTX z7a+U^gdehOPirl*EYh_IXW159)i=yERT;DN<-&gAb82x2R4kW?^a@~Re##rmiFgm!0Ht=~As8bS;1$~s0B`zo_+RD?M#ul*}?33)6qgwojYuYx$W8#hFXAA(k&oui}TVQ z-9_>xW1R@K(A~~zf**<fQp$<+`k?9v5YoUU0zbA~LG|Gc0(&ooAl}w)7s8eMZ z&GFpJ52tk7`x{1Bm@GF)@&8dT$Ba1F*Gz9-Wi*)Cgybf!ZRbgNzJ^cO(l)W9@&rDB=7 z{(Lvx;Mdy($~1`08>x{jiB`l4hNBbO6_jZ}^Fb?4o<5}^iz2upMS>R2nD$i`yPRI4 zMj_u;wG@5i@Wz&LAM6M#uVt?nD|2w`Rxz)K>mZFbvZ+YP^5Tix=TVko|**-%E;d&v~5V5VotqEq4cF+BAXB8VhuPPkw8R1cgsfq5p<%m z&&7+cZleoz%56FE(264~qL1YYh4IeI&_}=Rs3PBc#S9dbP>6~Lk{7t6kW>S~9!p&^ zWeo~Af4H$m(<)KXVYD{leRx`_*>2YRqejMcFqq*#G}7@gnEzTM|B{maPonyJh>{nS z?qkICS!rExS(qq=%I0yGkK50M6uiV66NnBQN$(1)Ol zKK#&madjQvUVRZO2XwFuKRvKK)EFBcix;w;PaBoMqO%x7lxaa{lNebRk0$)CX|ZpM z**Zkw1i`?ZW{f!?HTKdfQQ(6v2^c43D;+-QEi-*1;e99@Ho}I@(#U7lT`;kz*d2qV zAC0fp#!dT%dakN`9YN&I+#tK3WhsO3rXq`BNG0^Rtk0n}lx?tvyWcX+@JggHoXF?X z64N&W6DjOWIcduD;}*A9FoVS^ZR3c08HW`QOc_*2{}cW_Qvv-e8j@{edSzqGv|U3Y zp)9Z6CFmc8Yf$mrI{YEn+z%AM_1D7v&k5k~Vd(GS=L0{^2_X6;q&5RU>4L9k}T0VjCMW-M|cm?uBHC(g^{$y`%ZDHp6`g0;< z=5gtQfFGD=D4#7|{lQ^s$VceJ#yV*x2vE>49$HEXqw%;*_}E6=Nd7*WYg3ryBW!*T z846}+jx4E;O-1)GdZhjn(lFD}*u>Z{4nIP>{KI@`KV9xy4gNed{IQdyHo#KW0{#5>k=6 z@WcLr;<38p2J)^cX;8!dz{ZntQ+g1b6LA)Z2@}`bsJEcaGQCT zpNUOFl9iN9(IBZ_AqrLf@4pbA8}|SbPJk z>=oqXcZ7>yk0OJ#zFzo68!JVR?h@{tb|*o_7}NlF$2i3e2<-N!bBlnr!$)hiF0dP{+vqF^^e5{&@Yc&qAiR4n7DW18n5`>v{g&@)wTdfN&AKZpYbB?$6)M{6$L)P- zTM`Ms+AiDuML%yffvg$6t?FL>2_W39Y>^{;*oWLlIQ%bM<3Bnd>A!sXr!XkbD+eNo z=<@|~C?z6s5%nu(({%41a=x~ZBXoGUvm%`v3XkJywpSaXU+DW_FTqwUGd{XRUNFZ% z`$$H68vD)L)icBvhz@}bfq2mIc%jBlzOecFjiOeJ@;PlD9@PN(xUqu0p!-oQh9j9M z`GU4gh2#RyvO%x71^Mfq`T(wspA}D3p;QzAB@dR#nR zN#)>0&8-f6XmJR7+j0{vsIE)G3QPz*qr^Q3^id$(Lz3d!+ zkD2n0cCHr2CXS?k`%cW-&cIpKz}m&+AG1iAes}f;6)iawLDV-0m_idM@G^f&3~-ys z+2oA1J;OX0bV>~A+X1*UsK_(xx^=@>=vScZs42b-LSzDO)+@b7;m_Ge=tP1(jD-`v z+?y*bH(5@u_v1}DIbA@Qz3GsALG9*3T^PP=3-)q-{-K1YqF@TLkgRYPn2U_x4C)~! zr;69l+n9b1HshecHQZMz97Z=Y4NP;{m3_mTO78`PTPsXbkx*FA8E!>NNzx>v1qaeH zR!{_usu#sUV?;c)TdZvV+ODDWz$t=BN#@MLWpVN_prCWw9F{15U_eel9ZAfC^Ge^g z$h{teRD!EYO^btzh87-4GPKpw%rNAZPW~B`hQ5+m12-`ymO>8)ipfTU53ozTu565m zV9XURA6Ms~-P9ovaokuA{QN8fuL-^J5vDF+ffxszpD3mZLWsY>l}by{ z<45I`!sSfa9?l+n037z(cS^|HQu0!poAFAU?>y@Cr^YJWNm$@)0{A`Xz;jrle%Xl} z77cESM3-QVAI#}7vR+fS{+I?VT7AYQucF(Ke;2Qrrcm~wd~!xyjQ_+KaT3)2`F;nY zHCIcE-E)-2J2djmABziwTd={&X=J4ewDaI=Iw;!3Zs^6<+$&VWkiDNMe5}4q zp{d=q_SZ`~>`aGWUiC>7oDk>YeN5O*&Z!H9jKZksjDS)=E=od}VFW^lMK4x=iD;!_ zsHYf7aIhlv`sdhQy5gNa*tLE-sUj4sLs0_W4jrk5J5U8Rw3C~JeLyKCFxb)gHzTBG zDK(ar*8=z8cArV6C)hvws#c5f>EMT(@qI){&j00Z|L`Fh3nyn2Ta&+g-0%M6BiCcu z`;k12l;#GB@7D;-8P>meUMq{p1?GNz`HVFh(u~7~!(SEZE5I+crVj@0fN&Es)c#=l z_NmJcXwW@8t{)RoY|$I5!>GD)#nUyL(MPX#Bx|9iETioEWce?VD-Y>HZ8k?_4fdwC zh+n59WN-`>=;Rdbx`<9}X^Bf~V$|D+sn#~T%o@jvPI`?oHaz6WVq)ni&enHk zVDR~WA}cC2f+7C7!>!l*H1;xdg5%GEOS&w_Xn?6a%Nw61J!GFcDFJJhxD_4Gz;m;E z&0&NYhsdJBSz2h)tUg!PN*nFc&UsL<5TD7Mt`|D7oU}7YEA5ySC)ZYKv&AHGr1P>| zjjKV>71!9rL6J2UXmBc=BP9r^%CCOI`la+x+1>9~Pa?_Gkmx4reVu6VsBf7uR!U9U zrywRur{qCk?W<HC(0puXC(S>A7 z3#vx+5;{7Njm8m0^!W9f71RhqYA7h`5_jNszpI3p@p{I?s9o_Pf}L1<+$+wu3}Y=t za~fyzi)^R+_NkZlCa3tfrwfKIVDs*WBz0h35d)|=q&Xv@R3YPmbTlD6M_{;M`S472 zKKlM}M-D%~u$3A_3Q%{FpfnD&L5p^({xqNsn0$1*$fb+0q&kpmZC!Tc-Zl<~{$L>b zkXpqwYPjGqjSdK*JS01R_M`yeoL3WSp@ja6gbJ<;xBJA zXZId^FVdZDpsZvlhBgXiOy~)oOXp!BpMKF{&#o$8pdphG)U7d4- zvgoR#b;KRC*q%R?!B_^;OQb9{h)EG1xGwshIBysX#C#Xa4xw2GcqrC-WS;rhN|4hj zuma-I=pH8(q|U!iKl1yp;NPySd?Sxkk$cy8AR>#CuUlz#=b_KLAVQES1{3}2L$M<& zeX^51;(1X()Tt}x$GipFJ?6fA4a)M*Vpi_70nq zC9lzH=TQ9_Xsba;XHb=g#E2o?*c!tSlx?)#V`)sCqCfI5ZYGRe)%vQ)oJrP1J+VGY zC2Ulkq@Ub6xfVRb!<^4j?PL)eAr$0MVsPk#i?DiA8=Ta?$PJo3YKy@+VCikOM(KVA zw-+9i^`_dTxs@;*ie6nPRw|3SaRjnZ)wC#$bY$dh6z7 z8R(8Sf#2>GU>H-QTvnE1y4TU>d%B*=6D^+vU17K07Y5SM_RX$7z3VAbS*!xfO$qUKEC`J zq+vIvjD=)tO?wpB0q-82`NvCc%X~Mcp-`vU4ubPvWSEI7aHcbC(*aOiSmOwehu)k)_Qw|b&FMFK)j7i<%6#>a%6QeY{? zrsfoUh4Y^P808ZK@kc`LcRJ%Y2Al5vd_gl8f{CzU%(}TaAPyL=ZDG95%~4a-#HyS6 z*U5~M103TudiVlBkfm=Ckd0+PTEV=uSR*llSfp`Z&xMcyZArDNTY91Is`Ctvevon; zN!<$iiM3cg+P($Y<6Y6gFA-qLBod`xyMe?dv6Y?@ju9o{uEl2z&_jMweQC&c{wnoV z#v`v;dC`fS4LrDZRsGh8$R6$9a}*Snc3#PD#1kgAyx6`o8@(<&Sw>k7YjKY0sxAI5 zF-wZ~sXv%g1zVcp84am$$TjVn{L< z@dy-a?-gt35+pe{lM5H)2}{xGPvwP_8%BEc!&XaQ;^`m7OO^O~_Gl@%azI^|HMn#) z$~0QtGFqJ?k1sf|mzWekx}TtgFVL%EYpg0_3^IcTDR%MTSxDgyEWVCU=D{Kfrs#nX z^=ShyaXlS1QV<_MAd2gn-uAbx*!lJif-_+N72?>R3kUPl^45H(~|gMXEFjIW|Ri1>3!@HH`Y zAY+h#6lD;-IG&|(R-A+Cq25B0{Pd?oaZxk1XqaoEkS8vuNvDl8PJ*233!s>qBjSJs z)J1KM?e=UTzF_!}*Fx2R%IEY|hzC%IJmVBC8%$<3DQKlDk1mF1xDl4p0$#H)G->z{ zchM$r;YRYLW`tH*gLC&e3hAmMtu;G)lD=Qdm`$ zHe3@^p#Y<;pE-Q+2*TCgOk{Ouulu9eVaM9fSx;kr{iMlW<)CxODp(OTG9zBo+o0J! zxY{Al6+3QkKD)abad6Uk%u@%)J#7}QB4f;_a)q)2C}1#QN($3cioqQ7NAdL;vB$v* zpvYdrH7SQ2!udEYHVVu1=;ldSoCZi5>5|CFL;k|oa8t8(_UEBtynZb4aXv5N-k!J@ zgc#8@#)6~+c`{UgE^V^4wtva_KrcHVIM}G zByUK(&tKLvMeLDL!5+G7z9zCkGtoU-BK(GpHG2dh?Klw5+uHc6*nwbgjry59gW@VG zx^P=6_NB@rIvj$Gnb*HhW+}9AEG=Rmh$cY!f{vPB4OGjTtS~@-`BJJw&R&)nbXB~; zJy)XL>d^y_V(J&`Cmv+LjY4*kuv{2C?hS?rQvdDiwBRzivmQR7=btFS$8G+zwt@Na z{y&^tyIPpI{aaZh7O9a~^TWD3KFS)5|7D$;g{`rjo20FiiL=wcl8B^)3EPj_Rg!ym`6-65S^XFfW~edztl&eYBH_2#1K0_YNm+9FJl3Va{c8d-wxCw+<>RDVPv zD9Dx0j3F2^>-qPCU%ClT@IOw!#aBXyx_&B|>N}8#8bkO5rEEEG^Ay;0ntS2Vn{uze zif`{~+_h&bjjOWmKccU8%R}u3RneHX#*aXHlI6Ixj8n>{K=yU1V;T`ZAaGLY;0)or z71yQ`QQ;wy`uX%$6pK8x9#X3Cg@mNjS}D%0SA{6yz?as;HgmMhyb{>&Dzp=-0fAHP z^e**1=NRSg6PSz}D*Ghr?Wu6ENtdCbl5F`Pr)YA-jXuXN%_z$*KZmohSWG}CvTxqQ zW%Ze?BjAcn*7{Ei*iOA|6{9>`KbOX|ra`xHCObg3D0q?2-*z6pAb1V-8BFBxPAwAQ zO?!k12MVmT!-1g-O`j_^*>suX?HX@~36+&e_9@rY_t?2&<9$0fI03!GbwL;-6o#lzOP(?4lS zo5ByrB#&c#9ny3h@%Jt#OEEA>r}axG-n1CusSq;;T?$I}t{y(iV<#3a2eMMAr!m(U zZH-Ws(O8;}D2H3hhx_EvP>;n|0!O0|bWfPgT90JuW9xf zOCWzyf;V%dwBn6oOIQ6i_I} z?~KfhEH(b>a1{da;Q{b}>aFo^7!k(-2LcL$0|KJ`FMg`};pBflq$yO@Q9@Ds;96j8 z{os2_C3>g|psI7KbkvI=f}dG)Sy2-$Prh>oB^k4?ujl@Hd&D>kZtA%Db_|EdofY%( zhQDm?zh%)4>B2yP65$*7HE3WPEc0T2| zW~xwUZiw2Fon5HpZ|Z||EuDA+FnkIQ;*)MF-q@n-d`b>-_{T^UqbpyJvCxDD(gTwR zj%Z*!6(8Z-H1n*0 zcO6ABDb=7fFSiK-q)$fD$VivrF;S&rY`Bq~pdBMyzlrFvdp&^v+ zMK&XtYT?TFE4`Xnh;%^(nG6Ms?r6S+pHu8crs%Znkw4$w`7@ zi#aIwlzQ3__R)=1#{)rsGXUj;_fW^SM*ZxKApQkbPFMk<&s|CIf&Lw9{Iz^ht+sR< z7~hg3sE@jH`jV_u(Z0k%|Fd_r9h6R-)OSE#;9?i}muW2$bL*srgE6|(w)>b8Ze=I-><-+l!Q7w6ODyXe+=5DQN zb$Dx#l()E%?5z_h7g=I{?N4kDA!fn+t9vuG2B%#W3TGAO{Y`;F*EBRSfSU-pyDDJN zmEXn)9>3DZCB|KDuF#?nvC1krJ9#;xU|A@GS*|q8rEWr|Ml^POi`PWsX>tTYx1K$S z>>^&u+J-$lb34zzq`phAW;y7~YY$NU4*c60n1h_+_8@lPqoo2Zey>Dh`l8#CTCK-$Hh z500n(2vQp7-hq??;*15l?7U%*Nt_)ccmlD>Uy_Hom*rGzFUk!!Bjl%0Pf+Gkq4nRP zn&o&zU#2*gBh9C>>CC65uTb%}>70@I(R@Q(>Gi&alE7d11f0{|@&INW!`L7-;&p-& zW-j1Uqw2r}-)ufF$~t{%mMH~;b&DxXFSuouD#nb@;9E5~9{C2)PoAHA)=TmEL|(>o zC_v-)GMPtKxf4Muz>DP`5kM{X++ajCBov2B)+Ggw% zJ6oQ}HoWr6pG`VenhmW;C){yil7)Lv?zmRVJLHwRi^zAo`hsmGy8!`nGTGlhKi&1m z(w9_MTkGmaf}#0n|ND!&>fe%#jDd%pi?gJysr5(E@9)*--#P$)&oD{Ke?P9}SXpeg zpepcA6yOJGF4ZL2LI*=mqE>;aS5!b6imn64SZX|#mG?t3qgMtACxP@uWjskOlR#Y1 zU8&>dzPUfk(0PA++Q;!1KU^-^O%@mv18=ff>xu9F-VKviOXq`EOi`_rMoLY_!L3j* zTPbgvHVSn|A&7cd98}ikDp1q76F@wEA1Kj*i~peVEOjq@KmK5-7Xrx)Ki+Jw`K;58 z`N@kJyTF0*^1DHNf9btgY==TFpF6Jy@~1n1Gp5u;F{FF&iD;T#@>z3HE%M{^T z+)@XjFYpp8P8tjhMII|s1|h=x^raNNNb1Vctlzs3c?&T}IfzXasGS`@eo^NSgeXAe z`%#StsrYQqXY1a&@5GJo^pycEV{o|RJ!CwTu;QsK*6s3Qb{*?)ao4N{4uQ@=(Ta{& zf)3JZiu%zs&LLX>9?G*D3jI5f0Cl10>KG5|S4TX7=EO~^9f*0Z;#|3>`aV7GPi+XW z>`Z;UX1GBRrYnHj>l9f3uSPP~b#-re(bXmG8vLSnB(%?PV?e4?z_we|Y);+54#+

y_e7y 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

;ufC}4r&VpP};KO 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

G3$f?z!^Tm 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 z

-@hTVr(6U~618eQKk8Cg3QF9nb3pW`Lw@pFn^@gy~lQL}co8a5_pG0RDlczF9} zI3BCP(tXr;?fC@xoc=uj7$p>GZ(Y_urpKzNEP;iLZ1079!(h^EhJGZSM-^^|D_vVl z#YpT-b9T*ob_ac7PFV3!M2tBPFU{G3O?-)my7aU#cOb}+GG|DTD+*QcTv)dMHy4#Y z={rH{3tTr1yw$Rdnuhx9>uSq-7?3uC9dcDD0H!Jo@XH1gAkn`D2+Gl08AqJ)s}6Rd z*TvnNM854?N>iRswvcXeBYjrvHX+~{Vush*mVo~bxZ%G8 zZul)bURG*&9e9zMv54HAA2@lhD(Za2XsDbHfs69Cz#dc8S3D9Z8;{7j!_~q|wYku& z?_UZ=TAnM~N;?xn7X|ThMsJ6go zVQH3J{0*xV+RB$Fw!Jq9LsoRP9T+h zn-pVlzpXcmPowXU;XC`TqyGv;Pqvqnr@lBA-Uig?r%KeLpK5OgSS-qH2^#x06@DjW z*0@7u)*_Dti3cvtG;W>z5n}1ZSB*v)Ii;0Gs%ZvLfV5TWZ=yw*p^9B6YRwKXQ&m7! zl5X!kH$=0}`2v>4(qQ=FUn8W_u*J283g=^j9bmYvOp9c0ey3s!p;LsgfT|z%09D+P zL7k_HxMpuM!>VL=>f<2$tQEh907j3GK!Vf$4h7XD9TH^4fkbV~<i*)!WGu$cU9fjMZn(q%UUF(4Z9D16 zZNT@FT9O+$qKb10naIl=BDQe%CisQ&9j2D1_+qdGpcL-FN$7c_vkgjy*D_63qpwx{ zSXuN1^|$qZ=KyAXowgaWU^v<#!Wr`3+fi@+{FCI}(|2wY3?zm-;fg`x&u}zP!2rP9 zYOQS_*7ohDnH$lnp)MX9-LH4G3%9t#!QZd;TR*^S`$gTQCjqJvMyiN1n&D?}0N%%< zI29`kQ^1eieWNc6Yg~PCSehejx)p%P@+B6&>Il8}$ZXn6{%pGVhfHetlbJ)&))cBT zd*Pr^mzXZVtzD9LSf|AC>%Z`!|8X5T+tQH!fcWv_6ZyvvhX1?Z=07kD6{=7GEG6Vm z(v^*s49T>7E66p3G=6bbvsG$eYD>sQG0fE1`WkX-a>&U`=~y#X&b5^l9$CSfPGlVr z5D?^q&;kCMm3w@lFzVH3et0=KMI?AR!RPn;xA@nsl?}tV4JI)kua>7v?xW0a%pJ}X zbdffLFSq=J1sT!_r;M9|WC%ZcgHe{zy|2Y_qr*zw(S?CWG z56vDT&w8*71=&zh|Fs#qJCbIQ2J(8~6j^fQ6h$+(OYn{Zq#7@6u?s~rln&gxPW%lq zh|As>Wa$lZ&7BhZ(=#2aJF=!<)kyPFSe1K?OpoVt;PNVM@+GRtC2gNP7v7$CI2dvK zLq)aK7Fy_eSAyr6v#Z9v{^H9B^yk@7Fs1iehXmTRr#sxpOp)4dd?X zs-li+6N~9i@}5LovCTY{!26_iD=-!Up5Bb%ypEt?yK%eJno*%8IvDZzaQV{Nh~hYo z^Ny#F9f5VOAtSow;x0Tl%A93ixC;ii9kufPy*Oj+=^Qz6DY4~7DYbJAynUMdjj5zd zcbf5FZeC^gM001JEu*|`+WS3)msT$lCFGoui4`~@&L=OrLQ3T0Gs8B!>D3 zzcTym7~ewP_}m0Sq2sFJbR}`RY|M-dC}ZacgXO2FLPi~y)kuYpg^0zh+x-LoCHOQa zQ`m7m8og&hX1JMYq z&%b1Y(01(UQgwP(hcoo$Y_<+0K}JX)4F&==5@ElaSlNvUwn6wv>on0$br3!fUPrul za!GDbj>nv#^8hJPQY&u>sMkfjC?!{{mjHqhs&s~6M?tw==|*&1GPFs!$$g?YJV+6% zua5$wMsh%3Gk-UtS#^LI5En`eUeb)!rbxv<4RQIhM>;v~GfXv$)WoB^0Cx3(Zt~fZQ$Q79d^+{^@ z&OZ?MTo9@gva`#TJRb0Yukxuf_DMMV=Asukro@-ZP&tg5A&D}~Py8JZ_zXk0RM5FXr8e8;9O zeFoE(zmeIA%vc^6a%HfI@xL32(t{Y&IlH6KJ5f_M>xA9|PVy%qva4zsM3^V%a{z*9$Sw&p*8>*R?Sfkp0br;%bM2KXA66pShRp+w$LaV~!>ZgWKwTy-q$0q%k zH2yiZ(Rd{VykZK>dA+DfWQrt8<=ik4>K7tXbU>4J?|8UhW)2%1NJ=)60*NRR7bP92 z*s^A_NK5sT{16GDxrsKf7wt=PjXJBFTZzmJ0X+A}Rnow_FNDmgM1F%)`C~rXu7Fg? z^dx_^mVx&Kjj>hWvg&yLIy)^Nq zCM(B}G$F)0oiTXq`SWg7CVXk<*D{#G;86U|_azp@bIR4#$}3g7gVM?OcnZs#(YlI9 z+eziDoH~?PO8fqJtA1ZAW7stSe{H!cI``b9?1MGZ3@fnEr*KrRM zniV^)ut%w{ARp1En6Gm3UDdb+4@#9@&8HuiUd?A<+w6Rq2vjaix9+GxHNJVL`)XOERHW*YG zd?1tyfDtSAMK#HW2FW8k>i;s-lxp>j{mPXd96W&jY1w-iE=8;X5GMA#CL0Z4pZ%j~ z8w)0jZvVDXZ>~P~wz99?5#R`g(-BE$tEdvfl^O z9a<#fy?Q0Ct(OTF!jNTTRLq%OdJrj^zYM~8(R4r&`3XAFHPt}}U&xdmYvvHOrR!6* zSO+%kE={{2HLb!_SMm3wVXr-_-fK28+Bv*_Z=LhcYF@g`4sT?h4c>X1oR7tg@^i5h z>87w2&grF3Al*h|;1@wK1Kwkd&b2EUejNoO4JKslpW|a^U&H7k_Y6FAtnWY3mV5D1 ze12LAg&zldFN1EGVeH5CB=XYq#m7X~NCgrTlKXiAAY;QXzJv>GV7@_v&gq4lwY|S~ zA@dn-hDkF}R#f7}5F$KSl2-B9Q7a9zr{hYKe;fGt?=YpDu7CbT`kch>mR0>59S)uTDk0rlt%eC%NygLX3t z*1j{M3)k=K>%XT~a|>BxJHF#6g71XUKT=M5)`s>rW`?v5PPYG`-qSiW(f+IF{r3d& zKN3j7X4Ym7rvDv_9}y)5*-Hl(u=PB*sNNHi*&WbI2gU6z3pWtpyUkF!b!#8WCv>NAL6BW)Zx3Ss3O~+8MGcjMS?Od@5$agqxXL_5Xq;|3|OvjA;fSc7sm}NkP<=%|Est6>d)he^JFbi}5vzFPC-14$t zxAz|~1CY_iT7HDjt&Ui#wNZ=ymluz!y9|fUyN{`tNvBNqx0ETM<7jDIg6HxqL-m-c zmzA#)Frp)U6u zt?!NlcaG1#sE5(chv!a)C$Q3|tk8Gai%(i1ZTUW2@>>I1uUt#dm8h)Ut`7kG*WS?A zE;2Un-R@UAU?D1N50zu`n2i#Akb%=-Bzz%moak zo9`-OquHhSO!!Z>OP6ambkX>@Dqwo|vZlf}_0p2WCI#IhDJ4}B-2AQ8daca?ZHB0m%Ct6jE1x)du9MAj4w;sTnwXD}<_i7lwlbHY&CcaV zY&n& zF)?lP?+0EDHdtZ$Uxz!hEjKSA-%om)Q^fCYs?Db}q>LOUyG4iW#aagxhu}?@?h?x} zfnlVMoHXxfeA?DFwU*MxiC#(P2t(GR*<;1YXbnrul2Rl}?eu1?B1li>8)Jvx_xlA| zOafpnf6I_r`dj(0$BRl4**6hYv`HRta%;B>5@uD8{@%6@rNH!fNg+3|L=jO^aF}0F zc2?B!DAXA%Py?%)OpG!gI;5g}G0H8qD#I28CrLf*d+>N(aYN@()u6Yx{5pH9Bm6K< zEi@$rq2xbO8u1QJ>a||0vi>pEMQf(UW!&@f_{hU(5J_zBFD)jJvfNAY^g0_dlh2;q zPMWia734XPlie~qNfU;Hol=kR(m<>nYL}LdTHbz*lE4B=jj>@Oy{v2K<}`oH|0Y?B zjo`>4ly{fG+6{iWf?<`}wR@gt`j=rs0ESeg!tS*n=le%SMra6K8!v0P@yxWhIrjKM zx~RzS*62;1FA4-GPmb>^6ecuRcvPpQ=+JrLqZuP8KQC(DLiS9VWHpeMoU60RfH5Xg z+q<*yq!hxo3Qs^t;2T&KjHUJ!0R1&&NWoCTytf+Id=2Bv>uy%+;8-W^Z4yC6&646U zAIbsrjx4n*(%Ti*9u|Kj?@Jz5&FEt_u}*M`7o5=wDJ>Y%h6Z62-*-Z7TU@t zWNcTA{rQYU6WNrM$w&CTXdyhdh11zx4&`Wfwu_&~P;I&}?NLVaVAmRb!%yh< zTQ_#amPz?GMtuxYlfod0-jIZZ+LuDFu<11Srlcsvm@9MmXvuoLS&nYjk$vUk3*EL- z028@II7TYek*wBrFkWgjej~|f^kBnPPA*9PzI9A~UbBSn8rGQ9;f9}#I;F|XZ&iQW zcwK*6;JMmot3M3}I?Q@o_&P*`P9@(5-x0G}DMl;w{KsA3P@@=X6+X`mRAL6N0H%C| zpI{C?=x*RVnS7b?DX@x;JTyiv9|{Iqf;WDcA^q0J*f9H2@f-*Y7qtZN!_gmKnukED zglPORqWm#?0gQbL0cEmUc?=je;)tXo=Q#N`^=h}ltQkcLdgH=Ydjn@TiUF{#ba;pQ z8LDws^_4;W8@XULVv7hG3y?D!YFU^T^+j|5covf(H?34El3BE|6G<#er8s4)tDS?= zlp&;?FvuZr5AY_(=%U&C`J_>>ec{+!EFEDRu6Zk#W&ggK3fmX;Xw*k%C?Zr=xSA)KT zEpY8n%6Zb#U2#|uzorZ-rv-Y#WG2_sQ$U#5f z9Ok59eua3jIPnn|GaIpGC9oPN1F9%#dwbuLGeixIi3aZvMxz2bj4bR-;F}zt zDNtQh@~t11VUgmrVvJ_R3wt}%NIuUfL1qGs#a%rt9FQV{`F_cU83CxZ!Tuz@)MeTjo+J*0BI4tV$nt19?zHXO06|YV%x?ALI z7&SS)V-H&GuTN}l6ceycuiIl?qOgO?jER}Hv9p1i-C+4u1~&x==t!K=KjYR7Y)Q9b zXwPzP3xjzqhYu(sCh(I{c=T0TC7c{ zFmZgoSoyf#u4GVC7fS@4<6-I|gQFdWTr;>PlHvQ?Oe9`MO05yTLxI9P~N=4>2mjh%z9PScQLVh zcu*R)Ip+*{ITSC$xsm_L_ka{Wl|!=Yiq!yLVFl^*u62#q9%Y~>$QklOuAVGkO0Pzq zGhiDjQSd&x=z-|erK!FVWaDC}hACeLpQ$`u!3cAK|LI2-LFs4up$EnSz`xinzL-ON zpDPgN87lV(_!23}>e<^sbHvp>y0%a5VRm@Aa>I{`gsdxdz3b0XE4dw~{jfqGWMEZ* zpQAABsa^2ToIRY&5}z>#6sg?32#!rb`u9W%fFZfi3lf!#KKfKM*REL>f8T9i3bp$5 zh3k*7GiGVc1zR=;Aj(Elk?Lwo{R0&0{gS2AlcYgC6P=F6zdBOJVVBoP!rJyH4~}c4 zJJCnSmQUA+W28{<(LWML3790{mc+SR4bk;yy}4_F3~Fbcr!pc7h@Z}fG%PJkHZ@ip zde5u?oTcZPdpNj#r%l&ujxydUyDqlLmmzxWUP;^;vF6wA=dM;F@DnD}<$Ktb7?|It zq>={(;Oq&P;lEPiR4ZsYxQ}@vnc=6tyZU@ES@RnLx$v(n@vI06@h_%{!n3&onC&60Zkad^(AV*FdM9qFK=;|#@uv&~ zJHzYSWe;m4M+{OBvKhjcwwkyT$$xy$y>Q?_-H!kDc9}D3Ht61zv#XU5ozPTEM(t@CQDNu=^|jW8MFqMTU7z z_Q7%3g-M_7j5*)|!@VQXs@R#sP;xFLC0PyQfmCRhdJ*;v zuWkIspk1>z_gu(*^yMD}N27jy1>DJRr@aqTXKJoon z)Q#H<1n+{5G2jE~Z6A4^nzs-0K&f5LE3)qvs$FJs+s_^aH^~7IOqOEbLU&=m7eD4_ zqJ<${_eJo=Yxoz`)I^+*(=Q56TmR{@PQU@VbMhXmpaUBKire7hjNKUwwtQEu2|&U% zkTKA^rsJ8+oJ4v+c_(*a<9h=?J@qhAvgIM;KPweN0W;kodA2&=pqvafcYH@*_S#iC zK0-TLKt_uk-?i&ujT+MC9um)X{zNd{BpoFh2YL~BHpI8ucU4cAnP~aZCHzzKbzvTf zr9nC;uS;uHTGyffyOTMQ&b9Lp{FETGyTOMVpZ~FSC;H7)kWy#vf_X!GuCRnla>)5s zhg)5(z$XTC4{>JXGvaair#LJM!|SQ+g}Qe(rMKIXez_gU%xS5}GBSTq+0Tqhqvc)o zNUQU!?RV<*s2bkx7yQXCpAXLSRK7lx!*Q9FsWsW0qOxB}-nxn2y0`jyj*{GS14R%I z6mRM-{8f(`1{gPN+KseFa+^{17c=Hg%$K(by%lKFCKIJ^w$9_413Uct%E5mdy!(4W{P$OM2w0E3$TwXv?%R0h|FSCaAFPuO zwSTwUkzt5qy}QNJgP=Rt_Z}hA3sz4GCxGMg`(x@mt*hag;wpwCX*`(1O68p6RMO~F zf@0NB(O91#pAbrn$OJ;IXj<@Q{SVh24T}oEq&wqCbV@Yv8?#5tQ>Xi6#}m~T`_uaM z`Irn2s4We(@=WlPdrON<>RZ8!Hxq9Bp#TLxfKM1!{7w%Ux5pYe?OF~5S0zU;iYY7F zxQZ!D@rDiuK2QYtT!9`UOa40f{>|uPkK%HmsAC;Vb|x(Re6wg5FOzHt`B>pPfBIEv ze9$DHJ%778)apA4JNH~3>dKb8OU6Sw!C1RQpM#@vl?IAnJmcq3s5V*Pnl`&CTt{a6>KKKFm z7!{?Nb3XM;>Rj#+!ALVY1WBy%IAmp=*<6RWyq-<>COW*>UH#>zl@*EeoA>Ms=(l$FwPol1p; z!vv^r*~nr|?m#%YN})SP_`X{Qq4^u-r0`ZYsnpA)bZbAfzmNg@I~I!{DUQXAOaEM= zFD?QJ=bkcQLZrIirF%awKXiEFxUS8tfgT~o0yQH}-7f-l$A?e=+lIk44Lr9V`pBui zZ$|Cats@lzGk@d3xT;CI?c&L!l`%?4k}Yf7Y%pCSOO4Uq%rM-FD}%8mWjZu!aZDC@ z=-J4(|7^pRkcs8bLA#&|Ha&TG3tOxvwecolnPoP)YeL`CVH)-@+0&|(?Cl{)FMWfa zp9E{fIBUsF)Y{hyn=3fKkzaAX76b+kEoUeWxd!OZ?uoKU)C>b+AW0|uk!3z<;p7C# z*4_$?(<|f-fJ@O~S*o;JxGjkcT^fTZd-y4y(W3-4dwYAiQi2^pmxMSRz?7l#A`$@O zyEeV`z04KD%j(3J$1~`}Yhj05%q$@(^MaG|&;=)bX)$!2cSvmqer{}YwOx(KcS`{# z=8B!P2!4xrRA{v^Ry647F5}mO_5I_%jc_slWK7=yu!B3p1E_KiKwygdlYG9(q@8t ztQBerS8ic3K7)D9p6i(Y_UItLOZ9Etz`}g>cd-RT4ZtnH)b2r*??h{e_I2Il1{SCm z?(hP{`u^I<PY~7c=)!P={Uw^m&ZP3l zA(7|m$f9>j_cKq9d1A@*C-pua8N!$5AkuO)orMtty3Axkwx0L66kF@mgm4?bSA;oP zwjA|>nIf$8y_10*yx!)pkBI84c^POghzTIiSpy*XL4NJCyeXm$wVh{BdD39Z%s5ag>k0(%{CKCa0w;zJ5G@n;9511Z% zl=yT%br-YusJLtRGIy8Ad1e-o^Hdc=+Yu_1L(+{6#RP7OZr=gAD5a4}>f&*V_eQ)vodZ4pKiB2N>qF#@+=0(X#&x?=P0s&{@W> zVqc-BYfaV1$3>IuO|Yt|gg-Bz-9G`5JMCVk7hF^qeQcRHQ(egDiY$o>3&wOiUSyTr zwh5V4&zhaJ6vTcZZ@)g0=W&Xw${t!?>00jLN*R)8!%7IWOQ<&q;_qhgn@wyuE zaVFoL4F8Ym;HFcVGa78iwR$0GP@Ef?Sh@*qBKkJoovY zJOpVv547yTW*5{V zhK@fv4T?u28IoxT8@s9=k8uEt^Eegm=Df6uJ0jGyl}1h$V2W8Zm-(34vk}RXta>aP z;UJDsD03CAndZZ#L_o^f_rz4P({=oR&V>@Q_Dhly=qA8Md@(?Kpg)NdjZW!JXKRX# zt@nM_UYvP-)cpUDj-%|&SlP~8Tu);cXLHg=Ty_N<^QKIwVo5rhL(L!+>5@Mn!D zVEcE?5T-LTT-?cm{>;)KG55Umnr~P#lHCdtLI?JwS;D8XruD>vu|iL;15=9F3^|hW zh?sJRJF>ZN0i1yFC~xjLnAqxV(9hqlhCODt)>y(GGL;rhGX))5Q$3ZO^ROCNkXm=Q zEJp-YC%uh?2YQwt>S6UTzIVVzt!zco8zlD(wr6R*{xF{PD=yjy-wc|TcDJME4h7~8 z%%GZvxo5M^{nhMuQycbsm&XkVR+iU!4xdX<#()-kuy6g!K%I6j({NR<5PPS^B-uIr z&E%x`BK>UToHYjlC9gXyWjV59x#zmn#8D@TwZ>iLu8Vb#8jBs4vFkcv2jHwoLxUNd zjtHPUlXyI~0<>YXlY$Z)0c-3EJAWA7--NK55FT*=WEgtNwy`0t#Fr6y@qZ9fV=Z_@acVP?odv@Soa52A0r9#%uX7)DL zRz}v2{~PU+AloAi^i9|aNJwx35dx9oLX*+WOH08XZr5jfH@HxfhO7yUjS>!qOZ&!^GE7!KSe;h#CjRn0)j=rZ!pafN%E$uJ zT7?A7(oSa>i-{wx9ujiT&^Pv~aehvaQ z){fucS;v3y>i=UjuR!I^9ccmGi_|@(Lz2HXTG!hY4yvU#7~IGYTo5Q5)&sP}xm5BdcA(8-tCVyxp-W5J|>|k2hB<@3 zfEU_w_S<{n9nqJvm5tEM}v&+fNsA+45MW2XNXfAtb24+9;N|nK(b5T{tTkTt}E_JqOTTmnB7GKlwqd zkeU-RknJoywW6Ou`#?Q&l4D=MY0?;po*M{&m6-zy*CX!DBh+!wqTP=5*DH-6V+|=2Pe)gZ%CPUZ$iVxCX+5$RJtU3Lq+WQaxEWDZW!T0lsSEBgrbZ*w_fl)YuXb zX;&SbTl@n(HHe0)y#g*|F20(%(>6P%szljo(y*upNzNkm1q!YUx*Dal6IM&)M_8t0 z>hJ@IxQBby6;L2*Qx{SuR*3_r@(Y(@3mCa_)N%7zzbKQ)1o@?<%}3%kO7-aU!Agw6 z>u_Mrl+~vWu8{=ICo!1Q78ow=oKY`YWC^v5ZU>gPgn10p1|YR(IF+pK(5`XP-O9F*@%{!FD z67Du`<3RA2JS8gINkkd(WhjRmYooAU^N+TQK&i7g-c&%sW=9foQmSyUhdIQvfvz6n zX*me8+@d?8q&=g{YJRuy;^S*w#SeeQ%A>yo3k7>=3@re)n3L6Hb2isRJun3|MQCB z#NcedN6M|9m0xK~ixHPm(PG8kq#+?I*ddjR6RMS2qxo~7R2~Tx{v=r>S=WQ{GlhW; z;&Mi4jWd~wQ=+^4_EGJ?N6lvgCz-CaQ}|a8IM`$MT9{~em?#lf)Nuj>)rL#(umZ9a zd4l9cn_o!>4s{$zq*&Nrl!f1jzXgy@&9)o)a38=#Z}7H)L?=W?w8Ki^{pk?%ARFA; zwZ6uf|J?9txNj$Fy@eDHvPKzkb~H1M(AwWmA>B!vmBO@8^6wMmO8tCVXxcteHbww%dCVVmVYqyr4uWIp`PLLvDe@eBLn zvkTvO994sdNNko$8QM`>q@%wq^}GwZ%ciW$0SD&cGsU0xv0G;vVEMaDzs#hjFhM$k9H9OKeM7B)U9dxnE{LUeK05+QtfU3}6aKA1Ehm3eBtAWW>F9!oWi zkf?H3lDL)p1j}f)(>fQL$t1o;?I5UtGs)q~WDjO|dy^c@NKCE%>R6}Mz3^S`D@Erh z#@}=Y{mCY!2izpAcZNSV^FKA_0hkuoztfJ|<~sx@o{HT=Qc*`n-XzyxP4{B^PQC1% zf<=6FnUDK(hfgIyln2g(<7)Mt;~8;@i?9nn)-6W$IJW6JusYL@ol`D}r}k*3+8^tFoLwowd$-snKJ*WVHQu7md-BD6O%i9r++0QD8u(i z!}p5jGx83ZPbcN>7#Tj<_ntwl#;HT|JXGvF8i^u*(nj{I4a9tPLfU!w<{wKJ+pmltU1d>f}Lr3?Y7V8CQyRQX=s)Cts&x>?Arhg-rL= zE0*I;F7;oJB86?9al22GYAeewTxMsl!aciJOt!ov$W< z7et;%MHkV2f|9HkPgmSMy!%=%m+t;QhFr9Tbp`)H>Sp>Tb#wmz3i}VYmw(%(|4Q5p zc`b`?uRv~Ask({OR4>zthD9U~QA$VL@2-VTLEsC0U;&TJ^WP`&7vl_^Y46l;@;zWT zE4aiRagDxWa>u=s8Qf3jDGsoAXHQSKKQi@M`w3Wnt;5RoD0_qbY=me~5vP0#te)P) zge%pG6X$1CR`;Tk37mZ#pffO}W(gSv?WOqggizjyp#Z1V&rz^Y#6UL6#5~KrsWZ); zjd7GKhkP2@H~JJhnU`!mBtpHEZ4ubv@So@x-`Uro6!x4OD|mD)DS)Z&qj77s8QnjI zBx2gMsN9f53fTAKt=}`CN0B4BCp(3SGTP<%+a8{fF8E8E!~Je~58do>cKoyP)Fe~k zC>=5lZn26z5YCIGKx&`!pwH~X;QklwIv>nWy(96H*M1DuLBifUOd(0t6l{FHuP?@vG1ZRu z7v{PSP0ue<3Yp33YbRk{cZzDeKfM@>MZZs*{o2;6Y=`cy^a3n~hpo5->y&x_hv-hI z3J8(kqVIq2=6?aC`8Pbff`Oi;k%*Clqm#Xnppm1IfuoK6|K8LYu@W*s^hg0RKhgP+ zaD!nzyh9+WZ4sRG`jgQ-sPHiI?0PEs@|4o?8L{6Ltk+FP(*7Syu4Vaq;#%UTVhh}?$m5FDodaKyI z^PgVB=+HAmH5`5QjzL7J=&{A3r~NG#>{50_4+2Y{Zo{p{M0$9G(N`*aO9({tkq1Px zs(QkX|vxDNg%z{8mdk$sE#)-EdDwKBs@|^6#M^MbqFA>Q8HaB*OJD z2%9CHhmb|eg~uHz{#Yh0SR9nF0XNEm!|~rIUn+$ix%dCx)K^52U zb~3?S9y7V0FRQGaHQs{1cFT-;{|m$W-)7jhA5eV0m0tQ*n(3c!@Bj45(6j&Nd8yd7 zZ>~CA;Ma+qLBp!K#o5m)!7`?Awxur;a80r7$p?wnh4^>6qvF1%9iij;rw5r`ZK!V} zDrha|+tfKL*yF{=0dhCbBDJAxMh}Cz>-?Q6{(fY9)jd6aN5clOq52;ggix!4v9O*F z!aR!|KzaPC4`q90u%i7Z0%L2Hey_I1a|f9$Hz$t29-NswKo>T_qq?nKGeq>Y6?r{~ zKa0|n<{n>f4z|7(-3c@@GM~#%3w>x*E^{_75=Fi7wYMvQo8RIL^D_+ayhJa}A+b6d za0r>h5+gj^>Um%{dW!~0xT_1-vR;3Xx>_lMk9!;*5P6ecYDh%6r(wMrl0$|UrioJq zf>bSR`e|eC{{pPr~VA>9e}=B^uqP#)G{^i0Q=vs;#GE~jlFM4tG}5G{{qYYpOV_!d=nQOUH@2{S?T@f z=9T#Oe({2MEt_w%@j#s>t&V{1Mr))&6_sPvwV&|%yM>4drT)NWKW^me!?cmR}w}?o@s}*{=N>3JsEI9=971KvCh= z6bLa$$Q%Cw`elbCHU}*fJM53&RX|{bOJ`Wtz#>pt;vTElSwQgOIDI90C+^5!i=^I4 z+1p4r+JTGa@R&g`_>mawCS0Z+4ZW+1i3!c>mlJ#C#{x4I5m``!iz8f0Th1c25f!kp zX-!8(GTRyH-aGn@lge*K(^ed+r_Z&c@X6~JPEFJpuV(r=L5CSR(DW%1+~ZyXR!E6U zu9AMc728!u8rT~Y#E7-zxy~0b$|i+O>4I$(u~!%+G>^toltV|=?CmQnXya#+!M)Nj zpJ6bdzfJIvf4^ATu($A@vBfq!MQ+ZKqgR=AfSAC}f)*NA0^Jt$Cjvp919FuBYd8xh zf%l^DuW0JtBJBoO#t(rLV3BeHnWA+q^j0&d76mDppA(M#^a@|VkMtiNiGv>ITu}~8 zuX!NJLqHP_^#+h&lQP4nwgIm^3_8&pD|q78xQsTx#k5=X1KP#OE7)$k5fbNYA@k*l zv_Vnjv%>PX%h6w9TP#q{it|#l-u}mF;ALLg-}t?BjJ_{>n*YOT@W0Rb2>CH-OnM~l z-}v}?qm=FBS4+`CCUVY-qTnS6`il9!zWtLaHN8S3Y^iqg4_U1zp!qbfKR=2?EfD44 z3zbAtE#96p_Z+G|&Yq&Oer)zt<%TyYmFrIgI$^R{E>jtlg zfhFx!L^;8p;$RjDoTxFHE*LY)U@o6Fz7+j>T4x?34Se`vQeyHs(}SL6m;}_2Zk0Ix ziQf`U>UwB?(X0+Kra}fqJb=o5#|q234m2tq9y3!BT6ND%Tw^SO*)0dH=e|=#E!{T4gW;L7m`4rf2#XEo~E9qu)Xv z3B$C^?!W5x%6qTFthUPp;erfX#Kr5p%lEd6BnG4D-8<$c-i9Fol-oimR1H6od0-OI zcHzkw48~X_V@F4^x9QfMQ9hx)I5;lkSd6Q=K;Fry{Q}`kGYF>*=WI?k$Y--Oq+A-7 zr*U;mZch?8JUXmZ#`zyBAb8Wy8RGjcIQu?4xc(0-;GcUZNl5&^*p`BLjeqlo{+dg) z)<99`J4LQ(HCKyX5DiDw*T)cpen6o0%xXkLtIaTTE@JkgLJXb(X1!PD!sg<@XZQHhO+qNogtJ0OWZQHhOTPNS%yU*AAJ-vI3ug{+~o-x+X zIp-ZQBVyjyNQyQf5Vi%izAvioS@8pT{ownhLrZFM3G#_*N&8~Y*=d7+NjTFJwf#^l zsqR8xxDyyAb&_55_Ozo)w`>jA4?Go*?$rZdQ*^L2gC+(08)DS-Y|m7Ei_~w$vemRc zxdZO@b1H{s3K|QJ^HDN|g|Zj%J2i9j4AzK`$Ll7=-Pn2JL>l_Pj7#X>G3D=?e-oT@ zmS;|JOF2yI@BrPiM8({6;=MD`QO=msbr~ffu(VR^e@E5boTMKS7=uyMxN7qeO%TZI zAc*e?kr%C7JX%T&P{dJ%6?1liPYy{eh%cK&*34%Uz@bphna3YCMi|8+a>8_}eg4~> z9}7Tm-C{y%K7e__7-E9^zc`M*s>%Kr)J%1|1VGhno~gK!lCv7+5b_|?Qq zd&GCW!C)pCY)Cxji2qDNqmA!+1^h-@?T&vD@*S+8wdG~LRAv7D?(GWvPlR6}Aqpj> z{xIdJ+1{+Gp6EHnmzRVcA!73k9lp#1*7T zI=pY@FblU)-*)CgNJNUdN&gn=Txo7DCT_rR1@DheycWHzmYAURS!QXXXheixOZ{fO zHG*j5-gNDu(vX%zVwo2sZ7@GTxL`S^pRs+!<315X_tqvvbnQ?`re$vxsR%E{M`z9g zEsG3FXgV>5T9~EiuNOfn&5_I1>;~&BA`IAt?_zbRGOZ8(CK&_TlF>J3VH-F_=f5!q z3+EBtfPV?pPbmzz6+kM84JFK@^{2FpP#8SufI$E0{VF8-5g&sG-R>2T>hm`-iHQ8! z0rDjzDS&U^r2cm+Ep20H^naK>{}psmpJb(iDTFlmiOA0kp|Mf`{o}}AIY6pJs`N>b zTvQ;Nf=sojsG8f7S%IEwnA*f8{>0@bu%jDC$Mt0ecK>?9FjQ|j-P}LDcWrsOx$~9Q z#rk2g)B6*+%fr;z4(AkuxkhxW151+4lQ>ukT8RwTbSchW118HPds`0${cLDZi;Z*U zDlmu(T8G}ynbIUHZ2~cwj|Dr}BQa_Jb#3tqS)gL4L46G-;Pm%*n^7x_4&&cgswySL z96j*e^u533)(k|I*M69=Y9PaGp46SLc^J-X)WwyyG|aEma^hw6_8U$iar>*)rw1K| z2`^t_VKr&XUQj2s`teX`G>m)c%n3<=@Fc2$Wi<@NIA-rOz?SJMfTSr`_1PL{fhkSz zb9iQX8V+I37b-83Hf12Cjq zU?~f2I5j69GTkfdh#N6_mU%|%5N%0lGb-j0BA*N@14FqwZqu)FN2;AfVvSwaqfBm{ z@J@X`@{zp+_#Gt;g0>H)sh%5E1O`uwj(_*Eumd<30KMIW7oZH;J6bfHOaxEst)w~WaCDVtk56Dt-(siH zDAS@$WxB9=c!~h)oq>=9T2uUTbb?lb-WDE=gP|<^cUSpVRqw>B7bkwXe0S(5y>;1f z%ex4b=$D=2JT9{$Ws!V7sndtjL2;@hEU)Gvo{W#;SYJJ5uNm1>5;pA@=rfg6LfcT` z=M*5Mm;nD#aWShNWKE(J;x$aXt#!B<>UQ28I0K$Xbh3JGhCf*YOl`$`SR0V{DrSv! ziLo!W!J@($P5i5s%^Zj^AuQdae7mlIDk@GceoEeaR~Sz#xWgGVMs$eq|H zm4Vww@}DnIroo~j1mt>t51)v!8_}YJ9E`b@{3k=bAt?fexqbBTHqdxU$aGTYUZN3* z(`cv$25tFwc^}LJMYP|nj2~tkj^ys66Wxm&10gN>*mB^V`Pvu1`ggL{=1(yT1K=O< zX7^js#Uleox*(nDGTehHq&{lbs2;7h9D63!NH?`j0<{6DUO7+p4U)|9hGw8yFvV@d zrpgpZ?<+AQ8bs-wg{^fI%%vu|sCop%r4T!#*ErPV{E%!dg{VhfnR`gXi=rr^=5?jK zkIVx0VSBKIG`cS~|9bHOrtFFQ3kDnimA_d1@8#wzt@)2Bfd87{68;No>_0_`-~O-K z^grL$#*B*t|5fBLKd-Bwe*xoXp+-_iHO|H?1}~RIQ&%jaa5GKVTu$JNz?M)___H*YkIuMN$F1}koj0~ zr><9g=c(|-bQyIc{Jj_LcH%LZ0Bl;16ag+Q1i|oFsZ>I7ja02!$t#wTBsNEaljRkj z6Ek>WYPDOL`U8n$Q#yvA))b7+LSnQxgBb)~S>BE?AwMc4ErF`YnKpM)j`p27ppuKW z_|uBY@kjOtt@0L3kXF-#k-w!1B1V)t179mD@D)Vnkb2-UnHY08NRMxBWAFS{6()|u##vt-6C7wFj1tMoQe&8Fx(h1c zEI?koNe4(A0RyC%Payzyp^N*J51Ytxyf*I0Q+)kUDT2^w<#2QSj zFwGkY?(wHP$SL{CF9 z?oo;WX13SDY_yn4>EsL0jG-^EA-m4AMR%nn0vN&2s;ZvqD_5|O)klLK7-*X#t(D=x z5T!Wt2!P{}TO%mc@e)w^*?6}{H;<6t?A26AUJagxQ}-NnT-fcWm%y1!$uK>FzT{$0 z$(pxJSakJk?%C;^!EeH}Twm_9ZX0*oiW|fQSb&$r^)@zlu*h%mv%8<%@dr*i-o|h| z=?5||H?_Y{CrvsFo@l&Wp`sDJ{2*#TZ!9Sd(jTbC;xk%*{Hqx3Kv|C+e{G$qudSo| zzZZkQ=N^OtHkOuObIBRJU_>K_&5t`K2ucyY{gr(t=&P%0EZzWE zB>?qy*em_qpf6)w;o{zL9#UHLzPJxrGHX$+87nExh@z)_6R+Xf3e(GcL!G`RG;lhL z6mXz-BX<3%fD9{;?o8Qvxbh`OGwHiTZe-mOSRu^idM!z^6lFx zzqb_zPugKSe+PGD<=%5Hzc)5&m>a?5BjghTwXr*d$Ks+)VLr z`@3K91`7QD7s&L#jXD1`32XmPD_|7y(HlI{93dePvhpfVBq2V;pK)bKwBN<LBH z##^o-+AxaCcuO%yfyOjTKF!()CmURSpggO)9EiO(D52kDx)4Jivrs>i(UHQ|hE9OZ z_6?$Eeu3()_wsFNcoI_P=u{R{gaI+lR5JgfvuQn#Y*GreodYkv){cA&}NS2M?Fx` zjeIrn$L8fv*>G{ndDqSIX6Ev-tks4d4!Equz~>BxE6lRsgSUVWET zUc$43Pn3JU3a=aM|}01(4|Qj!vTT=<;b|Ew2&*xtJaH#CxT|xbju%Si4m847+}aa6GDKw z{Y9>e!FA)rqndg`TR(O)Ew`$xJ289xTlWbXn@{?$Ub5$xbj$tkrTf1-Q~%KFb_Fd7 zpsz;c?qEe~{<4diAV@JRm7I;REI(W-XyDZD*@Yp&X~kKEIP80&J0#>^MarWKV|bxy zf7afyj1-T{W|NCCFE=mGA65SINIR;s)xY^cB*>GL%8G^<%COLat~iS&Bwkpc2ayIJ z5*?1jYl+Zi1{O8U9JXw15@4|!DYUO}eSbA};G!~S>*$qlA=>@Bu5P1e(56&Gl0 z=JqN-UbpqeI+H*gQ*GIuC(xsOvj5& zI(n*&{aY%l(WPrBBIr)L(RE7V_so|L<2%k09P_I^;G*O&F(Fu(ut9UpvkuU9g#KsA zpMg!twlV~P5bi~C^y@IZP=I=PKYGK?!qjZ0f;W0qgEs}KGCW}+tU7RL_n+j0j;iYs z>~J(Zzt)3KhNphgj>ao|SEv*3#KiNb940J?eFX^#YS}}?PX<9~e+7}_?>|8OMQBWm zd2-m}h654DrcQ<=u7sec`t$m4(VE`!AfWSW$&tR6T=Rdw!;|1GQi4;2-)XRGI9Hso{<0dq&T6v#9tylO7R?_aMUET~ z`4y!%$0jb)yk4C8OfoBR z{cs|6qM@6+owcN;4ZRP3e8(#`&=HOmS2&TEd?^1sx-(4<+*=2l#T9K8a28p~&th5G zDThti*I!ul9j-$*Ee{m2Cw*5v0seG*3{kNuZ&V{k9!c$-fRuEIPd;#+%jP>)kPns2 z5!fNfn!btTeNP;Y6Xhyc3rpM#+Z;=CEz%q5&*^#yCGjyAJifv?sM^a;?Bz_-Dg$@? zh@J$hH39~?cq{+d39W}Ryv=+gC~gWR@4R{|y_&r6vk79z%H3jZzJ}l7B7rGDFBQ5* z=M=si1WNC<7763Lzl!89lrY{m0Nr&}MmcHJ$godimzOM7b^q*YZyONq-Jle|Vu(%Ut5`1V@UO%DbGcZrrVC z1e5rjCD=Y*W9E3bgADYC-P?2QE*xg6Jh`f>5F)FjdNsOgbNxs>twdkNU;sgWImM4$ z6lRcKMXet@We$g7M{%v(LN)hOyC2BWc^?-)pq&U-0j*9rj5)H*>CT1okOcc{mmJz? zr0}GL1@HbPfi6@P()sNBB|R|~U#1Cv1d(hz2ih_12nk9QslAd6J-@8Wa-f!T95M9>Y0`6ssdf`f;Av(2)xfK=c*==-Ix#+l?%-PoPxQ)2c7p`}pH zY`UPp@1sl8$dP|X}cH^f-`QM_ESdc*MkqnubZWVJsffg6yG+jYu z4oCt_=vD(^NjdUsM^|akj`LOY*|Q8C&AdkRs1I&|(`emCk-!-FUL$K!I{r4mzxpeX z=r2$)%D-<_|F^j5|5`Lml>H|ZUbI?SPLX`TUu3H*^$6MkpO6?N2MqerYPlFtzR3t8&Q#QLz$~ZFuHvkn*9^F(2L;${&@$);Ban1q21Qg=QH+pmmy{SsR66 zZXt5)Cu%Pm(v^5W!eL6I`R9~z0O2?h&`uI}RzG-y2`01O}NmcxeJ%WsV-u4ezi zHjJ~Jhl{O+5gULal+Gy;dqA<|@SusWG4_QeFwD3FT3W)_$?^2ae+F$X%s<%W8RsC4 zC)rn<-}oB}K=qx#`{-*GXug!1=f79#|92G(?e$#r^ev5O1poQ*UrQ?g2*nxy6yvle zJA<{lJlP{uXdR#b&&XtkyiN{2&?KgE1xr)Qv%IM5QKykZ=`)? z29Wy#Xq0XyF3uZijSRQXqp#85aJs5>_4|9sFOH1H1;_n-TZtE>)(l9w;Im~{0YWCF zqq=8;-d(h-D0)}=oF`y6zaE44iW>-`VOupJy(ZFw*pVOvO2-9sgnL}#?XX1Tn+IXn zefO@juMifK-vonOCu_!~8N-@a5Y3}YQTD}w?j6f=qGIUdd?&Y9lLLH=&F#uwme$wfM&_fQ zL3*jaX;EF`UK>~&U>G4fVb(5H*&(I@qCbndDvyX^_5u`Unb2_FUHs7x>L^`?dVODO ze2`rC)IU8yO`Fo|dKr`3*R+mvf)JqFJ^(MpD1T&h5vf2GrlHJz9v9}KI@!T zwNy@@6(_-!L7`3(Nl3%M%Qr|N+a+502pY(i%Xc{32txc&zxeS)(hE*<@QKD^za>B3 z4<=)h7Yj|kmHxKV)$0=m6UP2(5GGwS63_fLtYS72_^}GOeWtf@RTrzYEpWTlTzK;< zd}gZD4(qW-vZPF>&2vs5L8~!Zh6^rtS2{9q7?@H~X7YCHsb_p8O@W-qpR7WkeOIn6 zc9d;Wf6bpF%U}$o9{$tk4;?fX?5B6Z!#$iyp#8(;q9Y9~!=%W)qy>tq zh1bu|Qq!kffCpcSR(;m*L1+&v>OCkWyF4tz$}zBfB;XU0yCB?@XbWoyxcFYY`rA10 zc{ygU!vY_Hphgb5^KTd9RtRo0h_5G8-@bSu|E7`r7gqlN3p4+EE=5oKH(sx*nQtw8a_|o9U8|C?1E-d{sTzsx%6x}mz zd^9cF#2yG@^spGQ3urYwu9B?_INe%Lh6kD#Wq^mB4lwt``8y}U(fQ4VN9dC-+>smqR<^xPsJ$? zZkEf=o2msYh?_eXCk&YyNh~zo#hyb+-VG4HpaF7WloGnu`~La56l2RKO_q!4;xLG& zl;R^o;q6UK=1>8e1a9RQS7MNkQZ8diVM%u|%P6R()xLKWbP`m&`;a_npl|Krq6%d~M+e$`dY5qz9@PlR5E%L@8N}adEI_ z#U9H%r7y7^EJr7*5)}28qPC-E<@Rm}I#m3Kvz$7O5E{8lSJfWOTt`$1zPH;NVa`Nl zOkT_lvZ)e-qGrTm$?P$pkH?P6{SiHmVi2(4)Bsc(DNr@*)m%vBwUcd|4_mmHsj;aL z7toMD#re@tJcapHl0T*TRVuXPCJ3`-FsIPY?B%xQLfKtiJId2#NFHC_iqY{=C7y`Y zKp}g^?+gwCo#xIL#!{(E>zv6Xu%FiZ#nm&+^0{Pb>bSR}h$JiBYR*T}<|)!evX)9O zxsepxFt8T1lJE7+pHU#MUQ>ms`+`Q-bB23s^t)VXA#e6RDt%$26|V`fWUmc~dAenk z-T?mO-cdS5Pecgh-hn!$Pte|_J1&$r9&dd8xYs1eJ_9<6Z;5WObkCUaox8Z?AHzpF zWzUV;*Rq3fDfIIXjH----RiB8!xSzV((A87R^qDKUopA?IflV2lIGrZ-2Z zRh%AMN1@X|E{|9N&jwS07|TS`A#c!6BVQ5vT~STWE=Xcj>o;f4BT}p%6(Z}-#c`rY zr!tlsr|)XdS&}26`6mQQX^rrpRNq&%Xb034uYY<>yN*Hm5|^!rge=3lj9*3X! zvaSrh*zp>1Vsh51Cr=(3o{RF3*&VlX7`6C-!r>2hABsXU_geCU9D0TcabZV%g*;3l zN(5DgcnL~b%ze!%CAO4N$ipg6h<+GvKgUHM)MP;yMW?NydvNf=BQS`eNth z#;;&DLxF`RW{zyX96x@$mzZ2WP;s9$Iui}C3g_kmmfgIB-N*7r?dY(ros#AwQ`OlI z)8=gQty+W<)-EbwW{P&3n>VQ_1<5XpSbFM9ywwJl~!UdB3sWRZ!D@(b1z7r6;8K` zH@_aArP5pvJX~F8k(;(jpK^8PQ%+=uDOeAjj8yJEm!lhR`Tt#y{m3# zye&!I&3VElR*^jJHlHw^XDg2mTndpY>P}pe{36dX{Zc2NEQ02h)q5d487y`HJD~-G zJ5A0CQ1ym2hbdzXT}d2-zmi4G+|SOe;_v(c%V*AygHbF;aF3G;K9-|X8;oLZ>|vT; z5b1q>2jy|}r=a#?VD%5kh2WosUM%A}#soY3xLNsu6p3^g$9nS+Wd%}$FzJ0nBv%2VuVh7|(F|9%W_jmGW|p_M%jZY>)~BhWn2$7_s1{tB zc03Disy>n}j$qS5raxQ!+$o9yn|wqM((r<~$P=yoo_FC+OW_%W*D;s2cY3JzXy?K_ z*pRtguJqV|+;-7F$~j&EKnUj~iX_wDe#p^G&$E6L%E1HFf(B&rlafv=Tr&m`y0Z^6 zi915$Dd9sCn8uNcnI;L6@sA@M6E5OUla_O6 z6H2+`)Kd^y637*WC&5se8xn!ioC{q z^kZ(wZIV^0#iS%>K?P5GRvMSZpeN$~u+%?bVla+ZEZi0CUE)eIrOP@Gyai&CSiCrV z27`pKj=eOT*NpMiWxrM@T(V9E(CL8YP>l~S9Hf3n*_EVAqN>dmj^_ZhHL*@dZ? zQoA9H1XBr(tSrG3M;)yjl-R8*hs=TIcD6@MbuhHZB>gGIOm5<_1%bA(`)W(oD{KF-_>gYPb!T2D<<%!sE zYZGGe!^sWMmGPtnM-ii9#~Q-Kif=8_f36D0%}FVo1=vw=6>Q@s*NzUKHrRPM{<-rc z$t1mH2se7|3g`4x2KS-KBq9^C8o%?9@U;Cz>9~FT^W-`L%}YC?*l(F~pIdE2{pqx`Xum7$V@>rdgsQ3KfqRJ{GgFEMP37AK1-=`{aqOYV5F z)#j(|dONQ8(8oN718)c3$rLjS33t}qLuQk)1*UU^lKbxNabSJz{zk>G_d{zc5U==| zEtpP>D6(~vYr2J*Oc+}8=RjMOt7y`P5qXKjhlBwnDJx9Zh*}9wtJVp!jW*;d33_tE z;^J2+`cpJ|&vzGy<=VVyMS!q?<%%ECiv>5v>r6rmCXWYqz5=WF5xu#i-)2>X{Kf$2 z&349_COeMP(kt8rJD@)*!P~=T5k6!yOCJn{hA^NWM&vd3_@{Sr)MNw{^0SpF@pw;0 z{S7GBo=D31c8dmuyXyj?G=L}FDMl1zEDp=T0@*I%&%&ISkGHQ^UT2Z&Rlp}-ljeiW zva8upgfb$_y>$9Nq#^GFL$vfDJP42*s#1PUk))7M5V}y8=0sm#t&Ku{I-flJibZ(W zt+hHGY#FCq{NSGPlI1BIyk%oRemwa6WRp!wK91w1S8GOxC1-<46dW2j6plqWC2wfZ zy(w^`WazM~pf+;MH6hN3l$n~|n6A+O71_LqRdc7gaq#jeej~5l{hC~5G&X}!yqws; z5`8<1SS;(IAa>$b7Ds!hGq0A3rcCjj-K?fpytF2>76K2w*)M_75qudwZ^HPGqzIa4U=+jKPld7V%j9KZyDez(swHUNRiM8p$Xj!2 zDtujkSO_}VR;?GgZ1GB=tymAhgLq?O=Bk1A%;ufFo$M;oGh@~kZ>s@O5a6J~*5!T{SFrSoq4`*{Mhs^%5}7WL$%~7^2<|TuRI{K zaRy)UO}Xbqd^I3Is&PecsWN4B_`7d_4!u~TtaBP$?3h#Rj{TB7Z=*+K@J$etZWXC{ zn-#6uK7fKm-1dgLb++n<8t=uVW;JyjWubML)6TBI(hAdYfSpU8r$zcOdahA{1=KOx z+GM%+o8gp7$FP)5t69lq4%=H>n&8!fsM>Mbvn@O>$!gSrrIzKI5My<@^Feucg4|Ob zR`PL30$ek?zj!B_xGH?g@UX?|-QXeFoTRFjs&tS}@~iRIV9rp&nd1J;E}1R4y!Mq( z^Z6uUqtPsx3DKeQbOB%llRC?yQbW8(on7jS*Ccjfc=8!@9LtO2!e@BqHLawl=mbuO1Pj@Gu6VfCl;8U3 zNd};Oooa2ad_r8B5ENyEp^4>!~KCimkz<{6JC?@ytSio z@`RWbys&wGv?KeiwAa5vIBB!JD5orpNw#s#5DQGq0N8t5mr$g6k_V|69*yuVUaJ+MkJ^q3J9%9haO-IT`pf@El9Ju7qJ;)gC2HJ5*udD== zx0M$3*0xtP>N1yKo+jhuaYn8@h_$e~SH+nm`xeSe3MVgY>!At*s=hjRP<0}U`}5w1 zXn#d5dVfDEauEHNj3Y zEng^rmm0F$!P6s?T!)uRdUhQZLOE4nG&2sjXahs;xg@q%YP>0XTsy)6x&<-Ez|aoZ>lmoUJPfq`V1>2vN{T=SMT`Pes!E=T9bSTz#tbGEqx zmkG8*!DSAUbTztE%CUv}Pu~&;?0VcPx*P3+DL>*5KoA>KYn;+yXWyBcNMy&U-?0gw zU)!;+-#7oY!l=y9rkL?nqGtRWJ^nX#OMm4?@tg)_sswma_zferzsr)iKzdr|8coG+u=GCEU8nXjg>wpoNcm;4)K#MS6td%70tjE@!7_5fGKe~? zTEv*yplr3_9P`yws%@z6Wcdf!voF$HU`FS(3!&da75slISy^rUcK&m~4X?&g3%j$| zSl-6lNW%R+dzM7%Y}kIFMiGB*cRbj&689?8Djh3vW#-I2mOkAy#KdAj?lu}_F3tm- zFak1Qk;TDu=GW`c=?$Hob0DoRGajea*^ljq%<(1HbdIl)|KiV}v`h-+|c*UjajZPIFofBbAZ*YW>?( z^jHaRi1J~F6J_F?Bk4VjnnIM!udc_D(wO!gcOdn07cmSmOA%~E&(qZNUPWRg8bryl+` z@=BW7kFu~&m7&~o%Ri@N;-*&&Nt-9o(# zT*yzOnj&~qAALAHd8%H?Jc>N}^oiE_IO5YT;hw@MZXM!v2_VAq;p82f!r;I5(RjaH zR(5mU?78IX5Ow}+`VNGVCu)^vMClcvm%6ulw`E{xfYcscMi3Hr4(hHqKr6}(4j)8& zHX{Y!58tyI1bfXb9J&cT)6j$a2BrsLuwx_7>|jwI`;w9V!&JK{F6`nnNOpi{elUVK z$169yxVC0}qiJf1AePki&4o%;R%4HM$sYL+AUyYbo!)z0@Hb-}!N<>kRlDvRba~RRUWy6W|9BJR z`AM1Zz#+tTpx+2cbwj~j|61sGA+W0V%O zj;Wjmq?0ZyX;<2+47I}!G|Ti&p_avGmo^V*)jBs?GfR#d96`iP*m}y$A=nhffwAjX zed-U1e>)%XsSbd6p`s^z5Y5Xyhb|GTfvw}O=K8LV#*Rh3)-FjUGZz^QB5^u`Q(3Wj z7*)d&1O=F8(}TGD(fh26>kX@1z;AL~4uOx$<31|Z;S;f&>D6j9zn5J&(@MM|WdK%? zXF6JJ7Y#%_D^gB6f6~xs+;Z-xzHHsBbekDN^!gdcO3Y=1#-ZMfcHl^8t2Jl@m_EX; z(jJ6fOTcS19BPBX)?%k0IvE;{xLD8ita_O%n^DYqAJn7e{NS;%yFw*YcaqwFm29cg zQN8{d2Z2@JFQ8Mpe$)#RIQ!M9nmxQm^AbJwig}pU0%ISZA;ep+qnPHFTY_Duf>QTsPS!Hiqq(VJy)+DSFtz zgI9w@Ehry5eV)w9lp3}tANvOW-Y_NS4X;<}?X83_+z5Bi6p$F?5Hjr^_!KNAgJJ*U zC8i%v%K=SkrJzU|;hojL_U@BcX_dSX0$Y~6Fw`tYm7i3W1~udzKbxI@Gd5xy;Z?X9 zk1fI*>`{^OkI^PU(s;Yjo78G@z>U72`F`=KSbnw*NooR2=?I3|IX*S9$-?1SDHq(N zHU0-o#%l&ODX|yHfQkOxen^(j;9}nBTl&tFvy$1#GG&JAHLQ@kMy#|oGzF*u`QQ`i zzn;85#!IEIaNoWOQTFuk|9be=)xRv)7o9#Tjd8gk zsKDuTtHo(C#6qN&Q1oN_^nGK1a!7wcElRI5njw-%tr4@@rM5^V zGD~BTM0WWSJICv?)LEzHS*+wq&74Wp)@kcYemHE-QqF)woO!>i^IopIY&IXJdnA3P zzxEyKJnt2LD^uWjH|;gqzhUEH-p;|LavktDdAHrB3A=XFyxFC>Revqkp7A_geM8e3A>l9NG7gIfc4%J0skb!@}W%5 z8oHC&zhRI&>7Z9Vq)G6TE)nB)a4Zn%9x4hJ_}6b?8E!HuO1`)qG)LQl(uttBmbG=RBK7%|k9 ze%;gF%Yil4bR!l38!4OnK5R>gEGDv3J@F)ppRV0%oXh_llX9Cw!pR>*;g7CNQfyv( z)LPx~8g(^n>%fSGX?~G=QH@vOoH^%FM#(F450-QNJH@D?3jcR+-s2MX0WW)WC0G8I z%wbsxsi53UG7N0`5~B2ya1j@DU^qYUfi39QQp(G{F?t{G9%S>3g z+w-<9I~_biHb24&q@^Civ-ME&9P|A2Ealz3P>{Y}bxV)IqRnxSyH=Kb?Y(@coB2<6 zc2N(aly8vtwF;jxh9+vvtW@H}o#WDc*z&6BYAN}a6G0&TlN`n{Li(#?rcZTjzcDGL zK;>;r=($suRDU`Yh0_R?(LEl6r}3=I`^k<-n%Iirv9_+8e>Zkka|$vhMVAJM{E#1i zrF58phCQ(Cr8gHZKH;}FMkZba7}dM|dBM0I^S>ecMTek)U%>5p2@oHmq<}nfw+X z%f~=Ozh{Ip9+{UC9^-KPXak0=ZY&%{#*oHR6DW7m{W*BeF2V5@4SZMjY*BQ!Y)U3P}iS5wvV_*&GmiUiEmo@D>d*~?A@_{ZtaS+P^8(lLiPxz!?_$LUj-mI zZfnUHo6M*~zBggET3Z~dST*uq8DkVd?Dr52x|O)&e>@nmY>wFVy>zrAxT z(Mt|*u=V8_?CY|F2fSLf-etXSRO+Tw552G8-IJ0Xme{RGuNX?sG>4^RTx74Lw?)aV zzxBK1uBiD(kWyk#iv^9QMNv$uNs)Pdmn_sgbWn+0ct!ZE`gYw?ZW5Y#aS79vGG|eh zVL^{g5qo`)EQQilOF#u;_Wjub)Pk%)prU^^hEjaC^0nWfQf;5e-+g?R(2iPnKLzS-#Xn4HnBf9U^Yw%ZLQVS zBlks;Av6+*A|RcF*}>SYpJL(Yup(2Wf*mijJ6D2u2r$^jtF}MA!{s;rC5wOKcee8; zt)x9M{*E#;%)F)Qx)mT|Wg;k(qO8gTY`7?`X5hs3{V>G=WofvcDt-j zNST{X#zaE5ZYI=p+b)S5bHbG?e6NdLgpzTPZ4T<_JScgs!!f3P<;7ALg6&i)_(dEK|0E(hj~Y$h;-| zv*%xJ9l=1trE+DP+;)Fs(b5%*wtt#t?9@i%n542%8uShTJ4*3JzcTN6 zRbLo8z}y}CrW*UGfxQata6`Q6#KW%e&AP5F+02IGW+YpWLeAVDuaKdSTfMX$BvDj<~sFtGh*8sw`Xv#?LK z;g8eagj!jP>arXzA%H4}-loI}TVqn}#iT z@O_lJEyX6g9p>QenM>thEMQ(v{@yH&?Ep1-$JuIuHjy>x`>JDOY~Qd)DCx%RoNX^_ z#77x0)o6XZ02tLmu@gWIiq`a0HHRzbZm7@Bijn4=y|11dK%gJKGRo3Z-sGF}5MY`g zH|*zZdn_$H{zdUZ2*+~S3P4dT6GF&9N9pMZIm<<>Ue5ZXo}*fSW(eD!?~XBEMM3aI+~cOHMbt>h}w%R|Q$%hq~US*$BMZZt6_Kvvjo5$Dnpk}X&p;XO#Y37rd; z5?1Sk-Uea{ntK1ZKf9~hq`Z1voUq!^IL*PsX^{d!y;>yU?8*7uJ+?cKi(DzYD=SCe zeEyww7(u6LSY7*|7uq~=;c&lVtAwG7P6Mve!OC2dt;NS$->(|d^*54 z$?P_{ru@{N@xA@2Rh?RO_l@m|aqetB`;iaiN_gVASzn)e?9}E;Z#AUvB8>B^i7gen|)siBxH){q?vM!!Y>&6krHTO|%EhP{dUQqMX#PO=vV5K?%ahQpq)>$MBFi z;y}SNr}slMd+wpc)5KhbllH3A_fuX`Exvp`3DTMqR3pocXryDHLU(aU>&ZFhG)E&0 zZ_z@13+T0@v^I4tGk2`mNmQ~OKF7N0xO=dpv=~b2b7P0@6mIp*fcStOc_H??FURV$ ztdYo55fDh%ho&?_Y7-Dx;<%21)}-DbqVvEUsD9HsGk3+PH>nCmS85GDnal9SGk<;^ zlpTBx8;2P#NfnV>SMA4AT|R+?kVNL%QZ!KMVECa^#8*^XRPXBw;Te7L#6A81oU;IX zT!dSV_eAIS1YxlwqxR_?M1^HDc)QjErTBK}PLWo;GXD0AJ<-(E4GP$HiM4@al7tnD z6JMBR065Zl(fGgqVfLzwTxm2iZ)@YwUwndW(jKsf-EdI*u$h4ZM{2fAccbaOhHsl> zmSTH`l0UDJtj%CK%p3^Uj|n1ycl@(SubhikS{OR8yya+VtJGYQ&M2dhC^5DR1G;u7 z`}r&U;MsYB+`_5eHSn#n>yMw2INl#D)tvOATm_5-Oe(;P)JV8=RO?bwh>-Pmm{fz% zS{XQ0y^yA(eR+ewzp{Fs)oxyZl;r)R#p>D2JJ6kJjg|8;b$5*vG!_lPR`+G6UvpiI zyo%`^J}l1t7VOgs{VeW`ByJb(vavhCI(zUfcBHb^|HIikhF98c+uBu0C8^j}#kOtR zw#|x@sw6Wrwr$(C?Nn^r#+P@kz0Y;l`|Y!?bJm|Z=kI5Zr;k3`Xm@W;Bjewj_EeEB zN6MMyJrL^&bdCifgnYMw1z)-u<$El})M4T>z{0iSStbb?+ESn!O9X>c0-H;)py$0( z$bP%b=g3^yN>PUDN3N>}(ke?v&akdKA71ZH5eX0#;)Od*w!Wyp94|9YTt^%&Lse)B1c^DG^h#VnVZ}DdZDQ}g-x>)SA+4BsE z94W$>7$+aVn@-Q{!L>iN{fvKgEEG@?DeFf+$`%>D5vT!Ay|HQ%c=^_XZ~2+ZXf`Fy zry|wFDhv6iK-INtyuuhFH${IFX5E0K1At-U#pSG@{943a7bdfr{wS9DaA+9;x#4Ugrdmag$a>r8xh1is zA#Vv&!SfPqAKG8PhLD4}j^DT+q*%cZwz?6!0`O_l{3@$`b%0~b=$5SZK9@|{U@>^V zejo0d?e+ls8MMzQeX}8XvmttepZyd$^V&H3DrWv}#QgDPoG)nYnEk$l07un&+dZ5|DQ!CH^Vc9IRmFCgT9tg7K#*S46;O|N zZAIiU%G(&lxJ3fHHYGRpt_YB;8i5BK`IFkwS|!dY z<4yW~x{i^!e@Egn>6a;1LoSCk)l!l-^=JdYdbG`Y3$JFW!BGgr@jMDgs@4qqq^*s+ zkiubS*2ToR_Kiq9W^y)Jm)P@k-`(Q6+S7f#`ehc&Ds-`S_>O6@glJ?m&(`Sc17gGS z)Yg6VS>QPj+VX?si>~Eb@GdQ?{qr27mj;dNi_hO|!=oulG)+)nzRdh5o|gYoGV9eL zy-?Pn2(Ku!K@wot%<-2?Y5zzX9rpvVIWlGf^tY$2*}K)%i=600 zZmVdaMi$-iqFwP@+nCqa*EiNTwx4=03YE@W$gqxD*I)}AMxM4kv_I)x{Ay?8ez@Oh z{enYexylZ=GU`BOt7PAbZL)YSisL#o80E*MvPyz}p;|4`Z_4qO>>Jna4g(n1-eZ#&clM?PnB4FYEyali$BK?`xtnNr z;wn3~o=Yy;)2eOek1ota)@q+OE&(UK*xSU7P-v@?ra2MGEm z7}$pLB@GxI6nQ8_ZnaM@%G5G4WUqDT(NSGAT9Gw0z+b9WsC1zLjFa;lH!k*VxQCLo z91QcF>6p2pem1cr=?o@Mv_|nDI4tT0T2jox3C#UHcmlYJmsl2MC)O!UoomsfZTIqs zzM6Xf!>JWd_W6V@>PBNF7$-=!EGi#D(&u3XD~A-74<#v0@BGZx3SNFv3d}<=`#dTH z-+}F^-kyAFeq|On=RFbnTiG!zMDgk1J`vPllo5uU1Nv_%M{$ga~V?6P;b{T$8!ch`Re+@I`Scul5jprTRLN}?*!i0KRM=vDI5w}V|tB>UDFAt zZ05#d%m&V|11~U4We@!@n7hWpk>zvaMSaa8Vy514kkwg2a<{3&=D(CY=;I|*k_*BI zS&xd9mDCuKb@TMFoFz6dE--a!{Z_C#Y3);feX!N3%VF2=1WcVO zAPzgv2B9)b^nn$i{zBfX)X|^O7uLV(J}Io@6;;C@E0e1M%r`~Lo`BYP!D|_@Wr?v7 ziAn>NMEjYyhr^~b#9>WQ+Ij+}I>3Av6it_ktW}p(O@t1|52Kkl@8aUQtn*Kup&HaJ zEisC(Jk+T5meOUH}JDUNX2QWE7|$pzIDf(w)A ztwQ2|$KZttV<>}ruuQigwu6kqr=zRmH2~BZOR|k!&O2SnIpXEoCcG?LVuBIE5sPSx zPVk7PCuJJR7)1pOVhD#78B(^XvZ$ol!?75r#hLqs$j4y}*U6jAcg0aJD~w#i)it7a zs3aHOxyantzAeP%%b?xYN!J8TSiO;8enbX0@rj(A%fK;8OXbw8OwvwI9E_gMW#!6s z+cCeQg}OmSmde6*_LgG#M5-OV=D>bl;|%PF0r@r~vEBiER*%r{Bm5D>TgyevlD!A^ zxxmh?3v0{aK20T3Kj5!T7Am@h5#xnRxwe8Wy~jp=chZrC#OWOh*pKcLto5#6EN#J! zZk!S2l(2S2l>>fAV`VXrLpSZhb<7MBaTbptwle@Bc9tz9s8lV;-qVLzHn1! z0%)v4Y9|J#8%YpDFXG#ekdu&Iir?gCpi*4`tpc9Nire@9Y?GJ z3M1p=;JP{L6Jse&R<_<%jBt9A5^18CutCh8F6X)U63LrNjHidN@x}!Mv=?vZm5uLU@ zPF5}bg%6Pir6XLd2Y9~}9#CBAL|o|Wj-=h{CN;>+c7HbqI;f0yVG8}dtz2_KE9uw_{Tbk%(cEj~jL%d7w~VJ1Y*Uu0bzVuw3od zECY0@^`cDMGEilkg;iVNl>obf@VyFFj^sTER*p#5s#kYAa`{raiT%3wHx=SjXE6?# znAsa#5Clz(5APa|FNZxnIk?&KcfI#O!C}$Zlb_*`_`y%Z5|?|-)XQ1GRVNS;xu;CkeQ4-F4dnNhhwOF=m(b*F+#%%rqsga9i%V8;#T1u zG)hYAj{~1n?zmKEDt2vQxsmMG7&E^d+dZwK_JVbcG+Dq?W76`yX$q8cMAyPhy_!KWr?Aa!n?0#`E4BBQaakOjc{9{)q3s;-ejzv zT$e-jGMxU@fp45#I>x}7W~2$_Xk}=e*SJVdNo_giQCX-fP$_^Ws^9vyBEs*amZi=$l%FCcF=q6D;zNiNIZskIe|M|he+bgfC zc#o<|UZ@Y@?}13O(TC#$VxiQI#aAV#1{$mNB3N#-p?o8 z&kEBS?9Wx?#%vPD11vk0Zs205kpgeTzQ@T0_ymDOfwVN^u!qx{?h+np4i$EGEXP5@ zheMPV5ml2!^Bs^(zBMHOz5|y%rdL7Zkz>w8VKSQ_)E!5?@21uqYsFf(iX|2N{7t(w z@@kf^FelZyrc{MZegNb>V!txE)98`Qg>3zzM?IQOgU~FjN`n^2(i*pgsNKGl(;09M zfqT(&C7p__&gJlm11ZKaF+k@4H(0LCv7s`tZr{MNkOOq?`1@p;MK`iTvQI|xuW76z z|1%@`?{5}~MeHq%KJOe&991n$T>s0{q|hn}`qv2lN){_a!<+^}^`ZcwIavi_3EG6-{s#UIKl zIYcIHjsO?El|XqoEYkCC-pv6l*E5X#G)gWA8BN?6IO3KlyCLL4WRwt|93Bok0GCZs z_Oy}tGl6sz$=S+>;y!*kF`<*^!P&U>?tN)b$)q6!Ch%LPr=Kq!a$H-!Z$`TB%)Eq9 z?(kIug^T=B=||9txWY9~&}DkvXoq~rV5@dfz-w-gw} zOT06JnDJ;RlF^dOg#9@|kv5Jt=rL=BkV?|_p45mJe8gk!*;QmAFuBaUs@fO0%@fo9i^inz&L1!>VA-Y za~F@o6p)3wuKE%AI32F!W8IzWlQv-F!yQ5Pn$#8hwI~Vphp+J$963lvG6UY|O+7qSMR-eaR}~MHpCUOG;~!rin;w@UzsNk_={88ceOJ$L2+tXoDCi znu?WGZiAAR{H2s^Cg()wvl!-vf-1{G?B~TC(&)J=LDay6N6eoB;U+dT zs9gFf$BJpxLqf5+twl?FzSwF3R`+KUJo_rKWP^&_GV7zT4XT+X`#6o|IdrinmMLVR z9jx(tjM=8-p)!INx>BUg9VwMj7|^TAO2yaW8Hh4}PT#`ZiC8F`1-dOY<|s!+ZQq$v zM>tnQHV$F@J?F$76;x#oVwY~IWuiG#2_=+P#fyqD=jZx5$emYiuuw5zuA3^HoS5@9 z(a44~6v?SlKg}z+S{Nb4$T{e1fR30-^czTh)B~+Ja%O<7C zjccwF8-JH8pU_zq;Imb{XEBzU@9m}k^d1w71*53lS(~1Pk)Hqg>2PS8MbWzM5p~&F z6mKuC59J*l%!&-$&~^dZM4R7%jj9|%&o)E zFKnS(Px_QY)AdJ#wbC-dOuR$bXkIxXYgFh|K^6h0&TVN?j&pGhsTe43MsK+qZK%2I zem(}5T{BwjBxT%Y3IglJ5IsUF^|q)~ieJcT`Q29^{msXy3D2a;QBH!`1MX%5cw^6&{ z^x&<+UZr$EctTe%9oT1Q7^}tGbzPgwUpIv>wpf`XL-SN{v!+vDpI=OgOTDp(6=&ed z2@s+ZBk`>J^)>qa=!Htb*yf=!H*E^TGh}6Y4N3@JN3Dah9VK?q0z+RuIfrh(F>iI; zL9cl@nC9JH{xkg!#%Y0#kQ2epIRs!l=5E#=tXO?4ciVOpKBqe(v^}_vH7^+b`J;=I z!k^Z;+E#dp6{hM8!_)xBn2WyKrAHr;x|%#8*$=ubP*<|OY3TV9^JGx1Zhz^EkDYk;xN6zF8oUdmvrEC07O~=p2%wR#<#U|+U@1TJm~G6d_FkJ z^SeiLb2*}utGBi?NtXZRbtuJOOv^@cXyDw?n`0vvE-S};NJ zE}*_}7U4cQ2(ol#eO&_jU+B7t{YsvNRO+p*2XLLO9b2cf=I!D1)5fMKoV2UgKSjP~?luB5hD_Y1?@ z{{^l)V#m&M@4kiccJ0E%t|YrjPcSz752s6?8#=PZ{)1=tO!7(^ZkOim+0DH>>>qr6 zWapj!Ew`;C&0D_7wCb9PpV@!#NoaMWqk2hnC!%_Z@WVipYt;gir*OZrSZcH&Gt^6q zPJXAxnkoYrVFyO(MTnDtMl4%NsRz+qAt9k@#{`&o=RWPg+r99K79i%iD~^p<*HBzhqlCkP zWD+Mu#gKw~`KciSW1QrUB8k)?6@vOy)L$A?EfjWM*lEwI$9JbY;IN=DS z$$&uqD(hW&K#W8B-kYF6YT}YzQ(F$l9;;|pHoc;(rnyKtqE^=h)>UpxFCx*@&un|b zLXDY9V=d*Z>LOrq*D5q1j(=2qf!WAG>`%ELe|s^91P|7M&i4dZ&KkC}MG3JWsQ{2TU@Tlr`)m)+CEB5?ojsv#c`u&~tq@vjrS)pMjegtkN7- zMx0#MSXTm20{&`xiO#3i&sga#%8*7&%92ib-Y6P1M)$kqcyj23&1Rgv&jq4)rZoi& z`<7R^t#x&A@t9YoP#p!ECr};Po2gJ8BEd@Cji_FMv`AnZB!hin#7`_Ihk~om<|U~D zC8;jR?|Mg-x>Cf?JJh+GFDMBd)Kvt^?m5dPCb#ElS#uhtd+}wR?6@fRbqf7j4P0|d z!~L2}V^J~FV8Ghrr1YQL&o2J(0&2-+ttsC5gnsz)Nzl}fcC$BA_$5vFe2P~_ufqLu zS4^mUg9NCZ`qxsglKqtq^8I#~TBw~u()|}#5U9WUGL>JG17E>@sa&zW>hyn-8>k!8x_Dr*OnJFlx$t2@s+sP!XN zE2EAI2Pxh^ITVE8F{mMqehx99Qs%~|lkby+iU@7}P9{@j_85d$NZnQpqT~ML59CS- ze7j;_14%Qb%S`s7m8EcZjgzo)E>D2QlpLNdglK6PHKS}f9x2F0VwxBgb8eN6)%#{D zBSES>C0(wGQZ1>!){kT0dyHV_S8*Mzx1c+?D?{aS9iHFX?CC0DZYFZj?n`Xgzv3Zb zUPMTdNdO(_@eXrn)FRCEkm{ST8%mf8uuTuCJAf|p ziWeAp7;WgP&kk)qqLj+UOwIA#&ShUDWP!l(e@>~l?x{KwKG6&r{az72WXIuqmra2b z|0Ive1jN56QcG}kWtz~v@su7o$=9%ua8(hFyHaZK5b_f#{#i+Rf58Ex0Dm2Hm zu{BAU4@=TyjAgQjjITRtQaFx=b!yv~!v*m&#}1y){CO56Dic!B0^rRtk;w=na@MM@ zCstnS`vl$N=%!yr&fP?yQYBGF%(qGUp}?#q>?&&PC`@*Ho=T?pmrj+EVJ!vOibH70 ztrIdR_UaJLUg9)=jRufboOazD;U&2-Sf@<6`CO?+|3$h#9Y|Acf!?-d7gB)Q@@gSU)w;UcVvmGwBp>;oE_mx+fPcbc3gQV>Q*{ zPcm9`cF5ob=+#gv)(I^iAo$pg{a&}y!8sqzT1&b)nPsqOQN2tZAw>@cDz<(QFHSd{ z=IN2V-c#&jNr(qf5J6OaXNPpq)z2| zp(vrbHaeK5JT_?M{7pVF&{mW%3ig7{Ccmqmjo1Lc!lBMWsHe!L+g9CfSg4Ev87yxe zCwY=RJ8m(j*UFL-r;#1B*725@%m?S!^4>9!(YJD&SL(%3Dygp)vKuGDOG*mBg zQ!x3S%q?;t9O*F!Vp$U?w(O`t8t7|HZ%(b*O>$vVA}I7h>eRR!Trr{>ZAPLRdV_=Lc4%!~!jEtb$^qvm$ECc0LO83sFu=rjBYFoD39!VpC-1aJ#Yf{s@23IWwgTYC}c znpwJ|D77NoeB>*?e~?rzGBe4$l&-FCb>t6(4>zsWf&i_PxmY&pEluq=-7DzEdre|J zGc?L1Po`q9 zvy1JU;yk)`$giR`>4WFVK9-pf;ycc5?LCpe z)!vg?UDs3SC|`0YG4Xz4_yCKcg@)-dRGwjKlUc8`j$f{Rk;meOw6`40$>j#td}~qd zfsUHSKb-LX`XZf9t6iX4$Y!pDqlJ-|;b09QfSKizIBIue&HAH+&DRPbq3B)T^OJ1{?R%r!S6fC!T{$L7 zxofEC>jJ{BnoRluUrG_b=y-F&c47Akt}s&_`1CwR^ihqd)Np?UlNu3N|EBK(Pv#MU z&magGR|1*Mrv7Ok65|kCRm&{-`%P4`7iZk8XP)|5P!+xWXg4ZC>NanCxbt77skm#8 ztFh0e&iN@6{%0cLU)rFeiL;}HiHnKjKV_=;a0V+70fc}JjP`JJu+%wLiWt&R2~KEe z0R#X);Sm`p4%NYVO{?y^7Su0(ALSvNA2MA%6zgN$`{NIHZ|>hNgR*<_dr$>bV%EdF z5Gw30RIMAklgn*2#mqb<$@SIcmwn-jQDgO0PIi?%~~7Ww$ww9vv12K~WLPHOPeVIcN@Um=K<%s-^T}cQai3&MrnD{S3M-Z>B8H z-DC$59qg>U!v!|bdmv>Z#iEfJtl65(fgLN%IkpIo2G%$mY;Fh`D=w#atyjTEt{`}o zl{5!G7BER!b+!hyVB|^;M721K$&-C%4U%k|dfeB~a5%QU@?D#oG;FP`a_weQ9UK`_ z*tVh!)mI0-q<0z7Pl`$T$tT6SaGEuph7w3o?Hi0$DcH2_4z3;!RG2ZZl2?5E(FSs1 z!wT{o;A$i$myz`56L8fe<7-xZgU@NGWCh+hBi%aIrwOb{@8hA<^o9tj)g5c?9xViE z(ES>$xXE=87hF=IZX|JAg@t?$v_3Vep8V!TN)+ELjp-sB>-W$PeUNW7s7k8iL^L0b zag)vHlA>{QY|ih_z3+7u2BGmgwekeKMoo+^C~ti>nFk2bOJy{ZtQ3E-UWa7&o0dZF zAz&C(1q8%!{4BjdmbBD@!!>AVo_aiW5XPE1a27Y_!hL>R7*0h<>> zQd=O!pqc{@SwejE*r&LB+397BKw z?w`ws2onn1El^ylq9gjt?1;!K2~5~$?X(vH6ZK<*A%CN@&m@~0t{(Za5#4rgQ5@=- zLe*S3Iw64qzdFVvCb!UC>Twr~cK%$|7u>RjXKbj>sj}Z3F^TfT7TEw$!6&Rl3)W2a z9VoOf`8p)ZqI@F^XA)zXhr+=F#mklCW$VqJAvxb@DCK3odI{|`LRBRs-G42-gj*+J zFK!pp6tMUsLA4Ej6dDp&r+KPHb61{V{!jq5>AkGu1DmgyfK)jBzCoezksnvae1j@- zgo0iJ&`c8c{Bab|n+;`4B{Q-V~wD8~?ew>$k5aUBO> zR9~NGb`JMz5n5maPtj+ARX<dAgo_gG-ek%uOq+G~gLO06#B$ zdi@R`L>PBmso?h97DbYEb1CHdg)kMf_IqP^6?RSpIbk z*}UStFOJswcTKIUxQ})gGom~GktvVIp#vZN?5-KVB`l0ibZWS`UdGwDxN2|q4gw>; zP!^F!cfq1U*@DlM6YobUMfcl)xuWc1&iSlG1FO8YKBF0$<{fO7rdw%UJ!UR%(R(gk zF&Twy_xtdTr9%Edg}~h0Nuc`QkJSLQn3@kDBe#ZwCb8Kg&YqvEN}L9#AGFucV%M@q zY2=<~8Z2Elw%%nq9?*&0Wx3-LZt)qo$MurRrwoJsoJQ!Yn~O_veGKa#h zY?_9)GOCM>G;PG9e8)J??DY(o$gC;%-jdV4_IJQ}9}>pbx>Np_{sU(3m#Mz`9bn_sLCyKN;5$F^ zFzbZm{jcI9bzY2E!&6s_J}%-Yympd!Y~W>^cC)hK;z;K2WL;0}!U12Tdh`H~379u! z6n;+JIEkXK^O7kU^1wXHpf7XV`wZ$=xTfpCBoQoRz}F7LU{RVl73~2=zk^0UDn$P< z499+=K3=1AQWwA@nQ=(;BVJ+zB6PzB0xg;?P0xt-TV1UA4-Ql_x;$<4edW$ zp#SBUOIQ3ybP3PO=L}xyRG?J0d=ClyUyx84Ov`p=y=iO0@HH54Vi%C9oGa28 zTZ{&h?ctZhS%bCNo|ByeL*4Oc`dZu}^_1~XX>r*#QRwGBW`KuwV|}F8;!oYjqd(WV zP?ZGWaF2;unrC{^XTryR2=ffctcFyFIEEoKV&1>^0uWp=9+M5Roqll=%DfP|k5)ZF zl)X*VxLO)#6XSq2vv)5G!HMJHl^5~4Ktby0B z{iFO*MZxQd%s}9@_@tGq9{|pg)H_iZj2qcySHup@_GaR0&!XOU8*WQ z5W;=YplT*ca#;vmYF;zY_uxdHUfo85L>C@H$3V6kJGx+X=Bp?!^URi>WmnbX}$w zPtf%F1&`)t^@P4oDTUEWPBjc%NjVb^0lp^YRf4qjo84l(micIgAQ!XpSYTP;0ESqR zN84w9V*9hff|QZsw0KBlo6vR*F?wejMM1G3c&cW45EJ4e1#*2Qqf+jTLi)I;LQ{xp zCH+Lp+MCizP$r)DL*-hwzY{sM(#pcf;Nn`}?HOfQY)69yNUa%JNk(@Qoj;&QG3$l{ zh-`W2(9X(m2#*K-x;vpVq zbhJJ$>aw2MA{yW2zsG&b=ysJk3FuxHIoNPsqh@gPk|fokNL>@*JOj8v- zxsIjl;+-WG`}QMNi8qjnSjq?l#T)|9-^xCnxs<{+{4yMdR>;Kf52Rh!{x>_s4#-AD zzQ!%tlGfA_Py6@^*{@AKO}9J6g)hm;MKf zlJU7d+eL$sJ`b06Lsty75-WK+CBOI6#?rZ%Tuskco0(jHz2Br>d@1We&sPug#IQD+ z>di{*ZijisuzuJvT|E#(czCG>h5uHC0}oz>4F5D89*F4)@I~V1GYsp;R074|_v_C3 zM)TY9G^Q$bNdl50pTvR>FJQRqx_1C!mJ#V*93z3AP(DE+rk+8l5miP_#&@JB zXHZ5cRE#mDg=|JFOGF<|%Hmm6Ng_{qEukt>mC-KMGBqV-EB@Lvow<$-byShYw?RT= zL7vkl$%VLVIrbGohNlq%)&@QZ!o1%codvgam?*+Q&1cx z%ra^_yfg*dZj`iN2kZy)Do?3cvI3h3)FyNvXGm^%I+|U2IA}pcr#pH6#(gHLy=ShZ z=?t6yOc5jbD~9|gM^swko}ysj24~s`-V%lK`>G?VmUV%70@kyt(waUcO7FJ(8r%$r zJ{Cp8fm^!rt#IDpz7xR_;mQx+Fk}Zh3KojkcHE`v7JBk}xa2?P^&l$lW8=AF56p#QVXHeo z(U{s zzPU+rc0gFNlKlORN^jp$KwIru+$&VFx~yuhO?ofuI(Y2#3l485QfP64%F5gvu{?fD z3{d-s@B|Q9I|8Z4pyF#$K*m}{M8~JI0X_#(n^M!?%R`}c{C@Ry??5uHoDXDc6b(D^ z#T9k0E(=3GQA@hg!j~j!WV9I*gjtc#S=2IkVlfH3n1Pe%PSniW)E$L`?qU8lf}kvN zpM;qea3V&~1Di_@#TJpnj&mEzX4;WT0N>&(^{S}6 z%O&_~1k0wHBD7{LYu^Jg4dR*MBUKPX9B^=xtj-+gtFQSR*sE~uw!;tzS#_el+#5&S z6?+5+ieVs!nm*2+$YMsi>ZNlAzgaIkB^b-e9Y7iK_4jebfWw$snQ3@n{5OelKf)A< zKW_f(&_$&0KQ%h&4q=}M?B9+$J4ht9%X7QM7VI_|PQGH|_4b@`_%Q>qF&|8?M39>< zaBQ#B@?|{Ix~zpzy9vHUWy{6$+~zY%cjIU0E``FK3W2;BY=429t=-3f+x`YOjR!Xi zz5QKwi+o~gbdTG?CvR}Hl09PMaIHsRUD%gf;Df6w!Ow1{agMWxyRHV@H3p2%!d4V} zSs_d}W}ScGv5Rg$A|I<$pJ*6zHJ+=c?3FZTLpqMq?VZ08&`M(641n#-Jw#eSraQ)Q zCu>Q=bupqk{tg}6zQ+vx^e7eii^SM~Y+=?5();u*|LVsTeD>|%az1xVLDJ4YJDZfx z*{A=92nrh$V+#X1nZG_|Oq>mj4V(@BDfj+0-|KHT^yzU{O0)6^z>QhxBNDW4$_UYf zK{P+2Qyt@_x)G^!x;7uL>c->rQx}*;-qarR#}31K&3?u0WDo}k(jl9hy2Qq=nQicx z&31nHynUq)4@WR2MOtOGv>Y0SPQA|?n8Iw?tsv4SV$U91TO6kT9#b z_NY$U^%x*G$IMrzMS}(wr;U-jv*bFMh%MsI?_%0s3lr$=?rSQfrr@}!6VsHHZZzD+ z4Bs)&A4l}Z4!#1YI4aEI*XF8ISSiTWsZ&o`Q@zsOAz3$3S}{QcJHjw4GeacKQN00$$)^^F#g%TzO|)Q1|tB&zxqLDZE7?6sW& zQpJNc7EN>_b%9<#byz3{h-24SgS~>jK18TK@VEZ&h75jF>13~=1&wjK&f_3$s#@G~+1#lBx;PZgKNU-5K zN7>Y(k=ZAfSwmzYKMX-Q;KQfwl8a~!ZCrZs`ai^-YskERP7xHSL~aMA)8Ti!#mT6? zg7V6wg$7d3!8~b{n$N-Y3*Bmv^tb_hps@ywVC0>n48iH9cw0&i&B5L|Z*iX7(GSVc zJihnBJ^SZ!8kX{kX@M!?dIvW+Hv{pSv&3i*5xMhO1n|3SO+Ar{geqh7oCvs0vv)9T zfN%@hY(sV}{0~}(3}IY{6lUfiAUXnvx67+-<;khPdv7R4eiEuA1kH-mQ+?b6Njsyk4R_YBG3NR-31SmZCZYr-GRxMs3NZPux}kB}9akm|<<-t(v}gXOf`e7uSm?z=!5 zAO5%iJu0@Ccpv2O?pgD`>YzML`%x1+^mZJr=HFWUr|Juk7Heo1$5Y9JMAtNFwngt$ zynAmCIk{Aw#oc>4R)zve<#-3{|%d6kF&U-eLOJRr)|2Wc#mSrBb!F%Sy!U>0D}j=^z*GryhUBcC99CylZcQ7`pLIc9a41#WvxZ3IFK70f+uMtKQWw=D!Ist zFE(EMCi>Wt2)-R8SSD!!R8uZ5_%2U`RpjR@+IQQ4YD0B_AAgIj@)HWF{r_NEvfVSpY*9gvGSAoa^HE9uOHngSIU<~8i>GOl9d^Kx~GagGa40Z zL^HdO%`-!^FCUu#dUE2XqCp?E%M z#Cza8sb-0BC|)v+jhJb{=MVHJ#bY1J+U)8Uf?!i{krznbx@2@UF4FCKM;*1V6P(0Y z{)jYP!~yzE{ZgIL@&>f*>#Y4QjefDEFDOVtWU;{Z#+gd2K6%n zMsWai1e|+V<<#qU)h9n^s7}c1;8HX~cHX89q3xQrBX zc2Sm|YcYR~un*7RH*Af`QCzLl@aUJN%zrK6ftUI5PoFX%;B$Sk|GN_Ye_UVxEa3S{ zT2{ycDDR{auC)eMbPJlP>ZJ=V$kX}tb_&^_;JW~rP}iV@OOe(+k}2I|?QOrG-{WZ{ z(xh~Zprv<}Mp8PH)5JsiN}670=i8S{m`H4ThmAgea7s6G!%AN)yyQhT2mKy{qE4h} z_Pu8PsyAZ9nNYWc;IOYS12^=LH;FJRg*^I2?iTzmRPFSLb|G%$#+mR$EgBd3M0l`G zF!Syn8l!wbVW-s{i667{_O*uGvL@#VgSOOuQLX;Wl!jNb1tu6#JIr`Duf{ldxGtEi zqKfj$H>5D$gAZ?3^ylLPSg49_KBt@dz;ij~hQhYGKFX?Dfi;8ciQ-Z2N2 z!0HW*lAoQ!nc+|y#-~yYDnS;_b9(J>KVFs7Mw=O&(aG8T8y{a3{5R#fs-XRWQr;?> z%EBp0*dE7+U#C0fI3QjG1;MCp)sVlh<~&&$seslYE>y|32Ba<~A>9Ty6;OBNUK_M7 zc{oNKA3);??KCipnHh?erE(%*H%feZlL?D+^0^iZ5NM8ttd6$;%#vn5eR39FL-I!r z&T}I`&kO|(w`g7Jprw$RTG|U4S^;13v|zGTR;u4vfcM77QQ1bNiZvJ>&<#fI?d*It ztKL4&qFjy8@kTx_LpoYz{^M{TefJoqP^%QpsIA5x=<}BScjKcZH>;FOtE+n*XK{^` z2P)@ws%K+@3E7j?yg2N%59NyExvgJmZc{&v+r%(9(4%CxV zLYEEwIv-dxmeWI!4|!2dhT$uG*&)Z;bLgdQuJR^_qz4(3Yb|scV+VBtp6$O%eiSDC z>8i#N;_LUYYaUG4xvBY?p3ke_q!~LicJCr01-LI@6d@6|8qdOYz)jKTF&VX!l*JBC z!Kmk1g4W;?2(P!oEGdn27SQ==VO{AcfX$(mhT7>c??f2k%e%@*4@d+Ty&UOEqDLOW zbPM_e1A13D`=|?(S7oD*<_?-`XbQy?i3^H(N}u=f(XOy<$pO>5-FyT$0i{=+Pe-sJ zB2`r%$y|2 z(dweeejCb055=tr5>HgHhp4I4cSXf54-&7R2Qt!P3l0^}%5MWP=d3=-(^cF8(6rW_Jxu%TaH538Mn6na+grCSKKnlu^+!cLG2k0cYme|5) zxkS_)7xzJP^58AGE=a()`ROCQhhE>d6mp^XWJ1Fi3C3gRrTSQwFj=?Vs^X*5%Tyv+ zl<@wy_zd6nC{E$T_{J&0;{3yT;nyEOXT}nUnUD+=L zjtX-iAYgUs_Hm*9Oq{U|nCb;GmN7Ye(4Adj6YM_sVjLMVN@r1dE_L|pYsMQlX=|~y zZ&x>?Y+r5`gL6?sk?;l)yK|z*1d#+20MyVLh}X(PB~dJp5|G$1(1e7mDbcmk8A@l0)#jQJ9t~M&w>W?1aI>*n((B!(w+f<}p+&|usIHrr5aAQ`%^7YZ zDSBO)iKLEN;)8FCT?&fb$Jtf&Ol9xNgbJFoO&YZy1)?tdlJ7DtRq3{Nk3AfSMJ5ReP zv`PSxS;cd4e1ek3tonhh02SldH=FWnjR=}@x1QPtO_#)zp3GmO&s zfhpX0ZCc&!_$asyP9Y`+Ha?C7(Y4$;!IZk_nCQ{O6U_Pcq>3+~@hfk0-KjADA8GFx z9ckNbjdsVj(XnkO9ox3mv7K~Ov2EMw*ha^;ZFY=Pug=-;bKbqbJ;paiRsFdC)jif) z*Ie^jYfjrTzF#J$f9U98_!SD&%|&V>tt=91C84QT0y8>hSD0UF$@b{8N3k%NsZH>v zOl643ga?~sI9`K;So>!`*NFfR6~o5IHJ#E@WnemK&OYV^r&Tn0o%#Z#WkGJ1?}qEZnJ7}#{K#LH2Hxc;L^T=yow=yf3W8f{>k=l8Rsj~Rm=i|7`<3Nxm0 z8$u;zwi&c2CO5gCHJ11B9WIbtV*Hki;*f8l`SY30T!<5o70ldxR!wqdHf+_5ubyV^ zWhOmO#u3xNixR%nx=U8FaCmB}edr__{wC;M4K-MsM4JO_E!kUb(yq!&@{7Z%^be?3 zSVu$0&9(VpG>@GDI#@>NmMc0Q^hFOQ%+8+yqN1&{P2rH~M$j&lsu9iDdXc$sUPUjj z%{rU5BU;1vUqxVN=|AP0k3{(LzMBp=VWFtQ-VMoL@~JC%@pT$Gl28{36GX_XyakpK z6+yVs>=sa*rxU7l?7G{^cEo!UE)M30Dv_$}Zt5kuJS#QCG|!>tQ%_CUZu zu-6U8W_XTZ9YQ<~U0eSccl|scrwBQ6R`pbVl5yLf(@2E`8YR>5sS!L~?FVYyhu(XtY5fr`v^ zBXNpfTjd~Mo~koEiPr_K-FM72YLZXFbUvbb$l>6#6mz9H1wja&=}yPn&4ZpS=vL0W zVnzLa!I~dW`8vM{HV<(bBNz<%OG@UujZ*DQ=?EdU6BYH(i|TjK@}f(`Ebj6nS|h)| zjDUni%$`gG!&CqWuLb|7aQ$zgHK~ZHwJA`q-}Ik>8&xOQ&j|D>r6IBu*1iGD!ZPU1 zuVqqIN#gdOFK;iCPa+wPwx&QF?g714fGSZjI_-gf+<1S?`KY;yk^d5tih+fWg-SkY z%Xyh;X|>5MgzIEpwaKEoH5o|L?Mem4;hPhcVg-QVrd(Zs$I3Fi;G(Z-hB4Xn87|tBmWUW@vwm4-=QOU(gy1;FuRX|WRm><^tzl( zot%IY$p7GH|7OeWXlHG0>iA!>jcAd0E#o42`l&*O6+++%^i6SyW$i+UiVX`lK8=OtsADRV zrDUvT>u=a{FoJZK5hZk|E}S&^pq4m9m8){_niOCT7v>1iR`bUc7`)CT+SFmPXa}-O zk%JYgil)n>j_v#0W}~Hx)ab*1A`{pOXr;(GjyryEH`P(7V-G**4A)c6Q>nK+)*DsR zPO+MDgr>3}+FUN^^ycp0HO#eJt`;{B)>gPdGQ0I4O>mMC!ubR-y^bpVV-1goSPUP80!r;ysepp%i*{?ady-4s1R04F=F`WiRUUg#2AdG zdbKQPF_Gj?0SLndfA?(HJsZJd$4>sBBLZC5USo^VyWYR{ z)lqtWwA;Rg@Um?**7vgdgzVkxnzayF$vO#C5o3KvTnWS?<`LxqPbXoeIO-0ZVDJ{+ zhpj?`n_HG=!Sbol0D~@$MPXdM9)cSBvekA{5!0M08)Obj^o*f-zs)LjiJP7_^&UQg zjRtVCRXVN3V%p7_jRNg>krJ$Kp!@F?}|7*tL zsJfjrtUbN1>w=Um#_EH`qldD z^DxkK`_fQU9Q}?OLxt6avL!84jo7wKL&=a?<(rheSPYhwnO98S`XAk1h2|oP?4iJH zC|8df`DDNaPe=DKXxcf8F_$NmFvNuitx+9z*w?_i6A$9!2M;V5N1Nq!Cs6%1H7vvs zUnI?t$0~n&vgQ#2sjXj42^*6ZbAKR(zf~a~oNg>K9peOEf(3|chLO$jZ-=a(I@xV` zF7hDBH8d@J%ndOnsjVXAp^g}eJdJX}?4q48*U|w-eUP)D^l3{~HKe@$WPoNVh;5*r zJ0pW6&APpD+7FKMe}m-6on4dcVt!}+!kAf* zB$^(VeT)aR6L>Ob@YpTJ6Y4&Yi2~ka}a9 zVMe%2&L*kYoq+fP!ot1xbROc!s~tX%qA)#qe{ROn{h96baDMn2Go}AkW=|0gX2?`# z0I9E#QMpeBHX+%82LjtIsFh|sGLj4X4lWLStm#b@fI-*VOK6&zKRTWL%i5_*i$3+z zNsn?y!hDj+-7>H}*}WQ3>Z9p^i88JHi06Q;zZoy%+pi{@wltJS-UKs(qZVCf{EAYq z1*t&?A0LjYl!^R7m-I)dnViI@R}c=pBcIC*>=K=w52eBMbUbS`a`OW1V!xK(vB_&Y z?K%t&Uefvu=cxdJ9BZuKuhph@$n&$;PCCGOJb{GKx_jKxM*-jgIHyAFBXO z$X@{aG76~iZ!Mj-lWtt5#+(XojKa|+4C)s~lE+OehijD=&zZ@F?#1{q7o<7$Up+^2 zQ%O3q$0u_KBwgw7ZxV!4DuHS>Mv8juizmbOJ|s@{&z3z zu?S+LBJ}``i+edjb8#0@Lr8Lr?Ashf7rH|!8^3==Z^!(2H`Kn@4CZ)<0P9VLzqdd& z_+8xLAARuKC5M7Zuf!zA##79ZjIy{*pN2`va@MAhV`!Iz)cYpP5NN#cC^)oz}rMlVp)I=5D0+krI{= zeE&<*7%N}nw*YbB-z@C^n^l5;+?@Rz*!d6D`CmxqANc~bM6pF>LIcDQnWVFJ&N*7P z(zd1A*5=Yunwf{3&3qh-qofJLX38iN!yroMPBhOoRhfL+;dk!FI!Z;+kC>H?@w7Gz z7DaOo=9I{G`mrevaaOpws}e43PFGfdlVn<(^`UZ5SgF6a7y7YD0b!#=5&YD*ZHt4q zcsLhV%*-9-({#U85B_{w#Vee57jc|AVd5f1fX9ueO^e+y%aO-@v(SfaGTlz)8Iohu zkR~tBx)`5nSS#{_G!j3q3uQG#i;%6C2HW$fCRVe2R|~+&Lk1CHXhG5UQVYlK*V+5} zOM2WUvi)4Y2V44pusI$r&{j%P}K-z?^Mnw;|D=6>J)vD zOpIa4#`9kq2j78tq99;kBmyg{-2bMc{+(j_&q(-R#a7bR%6tBUqft-lAC zz#hb^U;#}>$4NiWN_7yYWUtdMX)>&O0p^0PyB5e}$ZI^b_mktDt}>47I6$eb6bTmN zyc`C@8c7JMPRJ-C=;_j3OUD?MwAN*+_2?$c>)YexlHYn4r~$SWcd>(5e|>v(FgiSp~V=@ z;6_}{hA?=vTG6d5HVZN})K=_lzeJnqFKkXUr4W*sKGPSF8R^&R)f4`>EoHgsgr7wl zdva;Ja+6o#ik`6YF8wg!-f&&}^ESS@A~R;K!7S`G?3g7ZDOox8sIx{sZ_`+mW1}r? zE(aH)XJ{ll*IiTousXh@P~JlGXfl*6$D&+)k?!JWs`$rpsO{Xl~zxNl;UhO9qa=-&e-D`>$D8* z8ebm&W{`{TzTcK@&i&!DPF?uu4A5!)qQfQJu&5x@Ye_@~NIe6je^2>nA z(C}76t^xu(b$Ab_9z+_EfWxp7abmUyL=B!dB9oW`F0&67LP@H*R05|02Y8E2lb0JL z4Py1VBx}SVE>3HItFMaOf%b5v@#B-n;hq2=XumNXK$o%OT0xR={`%Xm6lM5HHtldO zdRG#!N3d2&3Yk~R!&SmJ@^(Ge8%pw$OJSCxbTk(B*8PPg6xV;u@DCq!A^|GgH#E*| zDqdOsL4SVKfD|En=67&n4&;lNZ9BM~1es^|pGRz1F)=>O0iVvaANzrK>Z2t4o-Y`? zgUZdZPc)!>wce8*c94pPklBWboS#!W9=j#(*c9^xQHxII*8C9(*Q1JWDaJP^l22ud zZ|cU^ZJvrmALgDk+6+FZp68zD!lY&mT__D_=_n{t?)xNd+td)SRtaa!>cM9= z)m5>GHBt8H=<@f*?-qXKD@!jozV!qR0yw1J^etI#X`6hPT{}Ln$)kQDR!`sBpT4sD z1_kC^e@nRB#&o`+*&MbL27kCFh6Kw*h@UTs+EPRj5^2#eBe$54=9J@yQv z9w6a-`^)qzx6CH$9+1=eivE8f3jXad_>cMi&lFdGRz?#?`@qo%(S^u|5So~9Mo zi5W<(#6+4=RRALuOh`3z``NpZ=HiQAob8`m(Eaj8E4Z-gK5{ECZ!R-d8tWf zSndcb`GSdVZev&h*M`8f^sG@CqTl7c*c5NfdrL;;8Yy)?`D~@=y1>s^U>Ao`eHix5f23zDcBP`}6To zpT%0N2k{6KNhJBNE=e>)A9IS@IlBsE}xbXTtufq)8 zGNT_-+z@V2LTcD@qL`?b+Wrz3Za(!EC3cqMjI?f%k}OBF-f=wN=s2pMl*-ph zeFdRxzWu%Q#IyoxS-H`Lv(ffy8~n^XE|=(V9hTx7h4WG-3YWc$5{o75Z2p4CW zJJG8r-a}Y_NKLuHa<|Eg^d3^tZ#!fG4(Z z)eAYIk#8DH(2DlQV!{eO@%Jx+KkEdYE(aqzpZA7vKkOLeLtckP+j^H5K<3Oy>GE^6 z;fG9x?FnnV=nNBfO)~{CxI}K#;1@h;4R=1N4P$l9IpA!aaRw4N7Y3#F#BkHTiTGF} zBhNYD>(~I=Q;37%LNJA$4(eEHv8Y9J&fAjpghU+=ET?KuPTkwuRa$%KoSnvm3F}l+ z=yti$s?44VN3YucfViy4!}T*_B%rEWDV^pE-8U z6j?}5D4I&Q?D^5t^haAwL9rX*zaydhP29`BgE#Q(GpPL7Wd(54|9qE2WDAW)6Cm4q9N{p1_8~a@AtpbcEnr3tT1&__3DN zh?mL3*gbzQ%VxXtZ&3N5$h#5Ats4H2_wu=IKhfR(1Mz^8_IDjX~Mcg|30TflPe*b|0VIlk+&Yak_*1 z%JV4{`tFJ>Yn+8?zwteVureX2!tyf|ZE=qf`5_NFWF|mll)2#r;Txq|~H#n#7*oR1fg(q~uH)t|nFp7nz zq;9ZlilisSb>)Gj#NC=twx0ybyH`~W;}e3PSL(aN_EJ&$=mSn0xyk@4Zsm^9=v(R_q7hj8ES%U<5`F_Bfy7N-ixH2ld=i-fk|9eMO6gwr| z&xkju-%zcc<%D~@5E0_Z^t88_;0n_R5bAw(vq{a;R7$t5^_vLv`{|dMs**|U&K%Og zu-_@Kcl+>h3$qQY$#K1gP}sn;hR4dob{A?b}}0czaKT_ec7hi^X0~DwE+RGBJWcY zOlIGtoEziDY%W5B6Gv4{O#E>mB}H|r!a!ZGuIl0IdKyLq0asANZlqlgRw^xhiXvej-{ktrUsrO{^f1ZXp3wvXO&a_C z??3q{5`_L>3>{!$kn`prlmlF6vXo~VKSVqv<@hlwvOL&kL3q*xO*pmC$v4j*h(WC!5h_f=E+J0?xxe7r@qjsU{KiJVl-b|LE=aYLES+vytIat zI?74H6PF*RyUQxZbtGEA<4~1+Jm3^_G%qaNm6Wx6*3*jSQPmVaqh^lFvUw(KsfZ({ zwB63~M%8^`a4MC4NLPNe7RT&NRGN%$j&#|Q9D~z{iVRJqd6ifqhx31MFAA&w>N{#BXDm^)@B$E-s<@{tAm^$rWv zatLzZi7~<93j=v1`29@ELn>2aa4+jViQ_9LKxXL72xMnP5RX_UPK%R*lt%V7Na%%? zr7(?mNFn_On~8tQB=HVwkaZ8%$UAK`b;3AtHnJx4#)aG%Ap~{EwBAHJ13QGk$Q{-- z{SF>*vmFPs#)IDE5M-vaQ^1*wWHlO`WV9Pc$D4HMBmJ;7*l|*QfKVgi={QPadHZPcDU9&!jQA{Ln-Epp3zR4G{cTzQOEO{N2(pc4;x+BuZxs6eAnUt+ z7%1Jg&be+c3v2Y8H5|>dco2wQk;D04mMYlYHmat9Hw~Ha|4S11@07*=z-Rr}n>+Ol zbztUy%A%mg)1s{k2?R7Ps)05{RjDE)!BSZUey4&i{Zo-K0R=m1J872Erj&uyR zW1+ilQ?A2o8T#X5D>;z5*t2#PQs7;_$b8_mh0p^>R5cL$f(Zh`29JGGXuMX;)Hn&B zp`+EAmj;k+lF=)l@vng6TSyMHGULSB|6WADoLH^dczD471LqDs>Bl#@h$qi=%Sq;n zTSAX;)(yQw6VGz^63T8vZR&&k_F#?e@}jC~tqoixZ9~}_^a&aSH}}m$RVhPHEy7@D z5W_hz$&Yg7k^<_e4W}SF|BaK5!6lbDWFKoBu14DwG-(L z=$!gA>1d7;wd2Uv( z`t((f6UiHlZ+>;y(Hs0wctwa?zAs43@hYgZA(j|IFdM9w!yBw!A+%((M6+SN@U^{p zh{&EPKs8jN6;T{_bmR^IryhmO^pX7KP-hEbY{4GN`#@9UYlc-B`5h>0{tzLR`w|qJ zP@B^=Xb?aydsT!p0K-K{sPikhoz$wJn^Qtl0BHs1%hgJqI%E zYwVUV%9Zqf!R4kI9?Z8pr7{DN;`+g2))le974A1{+<^zhFKZgO*7tJm))C*dm1^-< zMAqdWpiktR6De0nrFWq-x76}Y5{!sTX`%fz`Ah1(XUsIo1ML%h*BPK6E7%==3ATh2 zJmU7fH_5KU3bthVu3tetzOp-%1lYH~wFn?x#@Os02G~D7IrtH;=d2BMVl6vvd?ddH z-`9MlbEB#~xV;^r4}OIr>l81g*e`gBy1)90CT~Rk(}?{k{qj%94W!2hw!Q3;t(R>7 zX1_1?osxd9;B3g~CL}y${%&^)!1y><9%V<zJpSH9+Cxv{u$ca;Iq2dcU5>r@*ZTB$d^H(ui4J|5tI?iH zJ|!z~P$)#y7Ig{6n`QX!5yQqd!lX>&@WEO$hnHz$TOYlN#_Tx8eCZiN9<8M=U1Ovw zqJxhwlN??8LmmWs7y%wYnAW0JfByrJt;P)3S;%&@-bo{q_g;e^w|sH@TgZYAt`G~P zP>YU)48J%dqF9r;Mk(AoDWBjjU?D8tYF!8I5#Mkh92}nvRMU0=q+4;3a+Iu-vRrVX zIqGdtOWV!5Gj`-OnO1Zuzw3wn8q?_S72WSCeX_QM^Bc-iz z>(P_I2d>ONvih3OO2Yxg@J+S~J!NMSZe4Zwaal#`t=hF0T2xNadecFp_8sg5Q&{24 z$=ujD7MYC`w+|hIu1g)Z%&FaONh!Km7s%kIfTT{BvAHHUNYDtf;qiz>dSl+@AM?Xx zN`n!c1NP3C;EFfQmlarmG9#6ECJpK#vXq}yyFvTpJEfeU)VIgqepDKPBC7BX6o-q) zcf=r~d6({zc*`^@-9YoJ-eB{ar!GgWyeO*PfOo0fB(c7-bs2I#u8+mZX*xAxD^1zZ zFFp|-uW%V%DidDJMoL>c0(PgC4fz~m(D$2vz`m1awW8stcV>O+jaezm4?J2^8Z0na zS9loehd2MWu89_k63xXqZj47@oiDY`pt4#|$3(Pgo^3w1{L#YU<1j@8>m+MsO&D`U z{o8Xo8};lLZw@FvS6Qz;Z5K`89A_kDC+MhrtZxHoj|91Xw1vQ2qwzk|GQ+@=eNd_Q zSCFr(bw5)=l`Et;!?ASREm%70O${8!e){cGd80SHEBu-{`HeWg^Ueuqi69t~fWN93 zZvI>j5;zYdunE4h7#iliG3LC)_dAl8eQKfb?MU(~TKP<+;P;2N`~1ksxJJ!>Lalmp zV|x`i2+JuPdhK|FOn=Y$)05-qBA1ZGQnd@kKFqX-JzQb36%|;NYd|pCu$8;%9eG)% zz{=`u;WzwU9Xv0|;#dOQuno>x4JprRUca|1$>JK?6RWsy<{I^$**k=2^iLe65O-Y3 zagBk&!K>dVX`xoHuaFuerIWRrG0^D(fs`s9_IOy7yNZgNDW%jZm|0^8eO+)d z>J!=2K27WxZ;&7l2Co;b7&+NN8Bo&-TD-%n=XN)L4dGxz&k@0uPB2O)xp?A;Ia087 zphSA+*eYG^Gg>5B9DmQQdIlqe*6aFSxTrdtPQeehwZ&OwAlTd3T^hfzsZ+9*LEju2 zcuCs41FrQIl2L-)J5e?9_RoyZy}-ZEA$VDDp8|k8z2cZ(zVQFAd(i(s1pR+0bT3?W zbUx#;Z<615Eb%hVDJ?MurRQnsWHXDGrDSx^=fk3uLok?=$tOwU#jvAf))k=-?pHOpnClsOp}&irQTFx@>*y^4iM4{CK@* z`Qo~FrRcmD5bh>E^YgLP-45$2f;i~0;^{jUfzr(mCGk^ecnpkxOxR9-xV*>=0M_WK z5R0EIS)gi{#pt6Fi=P586|n<+nvd00xI1R#t2C%S@TSV-EeofQ*{@99g*+&xW6{fT zQ-z_^XUgQQJ?J)Y>lQrzIG~`!RI>X@T}>H^M};G5Ji=5D(~ze#S(|{}slW&Wqm|%E z&Qeq|8M(UHs);U71C^43#(Kr!RP6L!(AfJmC_rIW3Ku3BK7(t|o{7hFdGAF9Ktltl zO3Y51wo!h2z!09KO2Bx4KWlBUNjnkAOP$hgN=RKv7{RermX8(eZEy@jzOfpAk87y+ z*f^QWXm@>x)BDZtJbb-L)xx4DD6PpgJi--Lm^#Jf(_iuj=qWejG#AD+%kn-t88AF*X0%aNiOAQ_^JPBNO(3&BT?H3@Rl^>Z7%G7(-o zctF>C63aDbIYx1ceh^B>jM}fPM28EUCa~cI%LC5r-tuyK@(YSENcTx0O)eahZX&_N z@vpwV7P3d4vx03#)XMT9DywiY$Z#`qx=7m!EEmvueVGik?tVU2Rp-b)Gx=EFRV(`# z9L+Jq&1O0(`hY!I7PaD3HjOS8a^#{rWTS@?Jf|Tj*UJ_K>qS231I_|NjNdT$g0E@B zW3XcM7Y7g1aAUzmot)A&Cgpnq3TQVe0~v6bg=y&KXOWIxKM9S0;9?6i%#(=an$7|C zZf;Ktd1^^3ro~-&Oie_>{zwi}V;ek&U5CILxi+J3MPo&0FXCr)*KZbhQ#=HjYl=-d zo1w3xpe5GZ@a4!yGDk;_#h!D`1_CL(7njB>o9NbM z8PWO1W+?q;y=*1vywHW9@P}rKgs{xKB-R^s14EIS=%r}JKayV!Q@egu&qSB2zc*D) zd0z2Y#RRQ*KWi+X_pkcVoq)~@par#WsjCV4m^&TRk|4-Squ)83jb~1t$jy-ZcP=3+ z6H*|zEjl&~%ZhW(wY!6p%AY)>_DUQmZ5F%fbgKRdTHoc{PZII2>r~N}n_;RLk>#_1 zd2(^Vxk2zRHpFzLS$2L`fEguFaP;q2AUBpdOY`+g=c5qMrXsl-*`rnTX&W3DqBZAY z;$tr`A9t?AVKtK!0DC|@*+D_k;(zb(2;l|CWT_LD@&us4b?rMz>qUd@nJ?=k_q0@& z?AY>F7^11G@(zRO&gSLrh(FZpm zyHwa@!^FXH_m*bz&dIx-e35%S3iL~xp!gp5JHvCNhJCWUZXy(>*htUZdildTg5r}$ zn7S=wC8guwZ_d_bSN8oQ^914Tl9#D2A`qRB7eq!40B^<)+ zsJ3J|fqB(UfpUJwCc@1U?>A9@#KU1wRY)&dWDlD@k;+4SC5xiG%%zWUSr1e9(g>NM zWLs^|7VZ~yY-5d?-VZP4Vig!W5QAPF!oC+3MND#2nXwWVfZ(Jkszlj23-n(;1##v4 z;)Pk~ru@8!b1v+AF4$3wB7(`o95X@+iv1_Vzz$sr%*{|@(rvHtTMr)?QnGe?b$;F} zwU(#U^xH~H;1@_zg>m$u+CtK7v8Te3l728N!Y9gT_wDyLx_c53ZyV?;-)-NZvaZ-d z32@GU0;~i{oSL2@Dw6SDPNHm=zSeLHFf< z{QZ+Qh+b<4)l>*UVxJN&Gchk?hzjw!Nz1I_t>w|7VvAs8a^S?^O6`^7G3@(0i_2Vd zaZsc0H_PIw(uA(alrC%C0T;5$N0Myfd`(EEBOXStsD1SixZlgGR2DJkf>vAgAwmo?T|A8-yD&UG*FH;c|tRqy3| z1N~BSmt*Rt_;sY!9>Zl+v)hl)tCI}fVX$5CiPhVT$U*?(i(g>sj&a~?+B;! ziTP&{a87^-@xR^Gwg2b-H-m_sF>oE+*4gRrEV`kw^WS&D|A}qXtlV%1v3-ZyDjW20 zwMoH(Sse(W>+!_*`-&WKPM1lUC=tT43W>^2w6xCQZ>u$`GE=toJ}*8>==Q>oKYH=VIt^osqFtob*w1KOaEDuS8Pmj zh@z2v8;?gnW+*zG@YJO884V~Ye5(eO6#3J1xy(^kMEecVF46iw2?AFh1%M5rHooy^PZ_Os2 z>&bf8^ELU6J)7PT$O}I5bj3&O+((#${j&CHPK{3cFlZ2(0KhlXG}3R*W|sT&CdME>Nb! z;YIxK+lH`I7N`Zt8L>cOHR@9;SCU}fFX&blq*ip&*i%fpaPnkUvsLeP?L@Z+KGEe< zAK%fZ%2Tu1&NMO1!CXz+8iWcSGH~Nf+fT=wf0>1J{q*ZU5;pjwq_9*LKQ4BicMfG{ zVyvxzs6(%XzU7LaCNx#vc3?V&y8P|Ze=i3{rF2tUr(uk2$`P%0Xri%R-==rTyFC;4 zk=LoHXS$Rj{9=$Ut&7V}t!iClqM{AuD!Gx>j0%+6yzdL5>!6QB({!XA27!RB+PoUP zfC0}B_iWwj9dPzK0H$gtLK)K;4Ax-1cB9;)^%z3}wl=d(8FtO{Wspv?f^j`)>x};W zx-&Inac+p{O0yJqU~7;}%4UfnER9f)_I{9% zvq=fd&(qqG37BkiJXPWOnr}Zo1p)+co=`umw;7&j+B*W_A*y%mcyZd-`g=0mrTf<) zHhXJ(4*#4{)?P&rWLwR;8hsEuO%X1gnETs&7EG-^xH^xgzfDSMK{WRU9Ju+f$6`v1$Ti{H^Yl-y#-6e9F{H6QuUNXb{RR&s zx0mM_isG6V4_VDxDUOt(ZPF~SWf_|W3* z6Ao8o)%G?vqh|F1T7tw^1j~8GNCvfNmT4fMi?JAPR?h7k!7r-4Add1m+b2U-V2MASjcKD)bYbQ0L(|iI`C=6F|4+!Q=a?hDYGPo zVCxN&NkaK$8IZznejB}?M&DeP!*mnfZYv;ha}{ricVv?7xTFNJ$GQ-2vX!uRu+$T@ zYcmRF6nZEh6vz*JP^_~B6A`B~bVa8|E@!xjuUx-dt0S!da2|{ZayK9LerjC}ZroRk zcW(4IL&2{{yn=v=w=xh%nTadSY`6??@3>I`BSZ<7e1t~84?g+mysxnJHCJ(A8m@;@l)T{Z4}HP8fHcm zzL*7V13tWI@!O*_!0g8IoM|2LLHS08#b;-I^`yn;R;eMT^j`0`PBie#td=ukO4z-B zMu~KgHCm!K7w=U_)EGJ8H)p{+zuZFqNtW{#`3XFYyhCd$jn0ObALvn=(;jcY*DLqY z+q}M}By|LsCq8QNL8DdmSP| z6X~tu9&@L7`mp0l2C0!6THmDVV&ea6E>l1>3RR}Bf1Wc;(1@e+n4%9mvnW__(mSv$ zQLj)VR-BSR2_MaH`3RB4VC4x@<@?qf!@nPWNiKfj^1vm7XCPB5_Me-3H%k+9Q)dPd zM?*JLM;Sv8I~U*$vW>mrKTbYUNn0mVXD1m;CudXJe@WFBDN8&3O zB>Urq?fv5Qh~dlSjxQQaHBAfhplqQTQcqR3v$mj?pja=2@V-B5(PJ!8>f}6@5ScV4 zoD;fb_`BLK`b=Rx4vAkHbvWy#4&o1&pr#`{arDBK)@^-GG_!w#sPG^ftx*`T*ImF@ z2-I83c#`MYnoZY|_be}Ls!0AA?^w#?Z6eDByHzEt*Qc{Ty4tj=N<;}^U?N?uaRVdJ4?Fat*KI24-I}Y$MXUL`d{3~)-(N1$0e#}{0mI)vxH+sKcqxN)kXoQcf zWe%WPz4(HU!?Ix}F&oOVyK^pH``D#PhI5m|HjbkDiip$?;;?E?3!k|~9Ex?@8Kz;6-Bc^!}&T^pXl@7{#S_~lA6#eM}bb%0roGK;ZO zS2WYRCe{_wpXE1UAz_|@te{e_Dp+W&yV!%GaIKvInA1d!4gTE#reydw!eJ?qa-tAD z^ZQ?^WJz1472M>V zI6MCZU5Qw&D#HYNjef=Dm;X@B!`5|`ozF?&0w|w2pKr(0c7b1v`1^(|NSP$(;J%63 z!)7bXT?P-!ionOmj``+%Oj{iv1am3HOP{n`4TS8_VJE`1}om*`*pOx#5Y8I4U{{7IlmY8cQsX4?|x zXUWyMQPsp))LDmPr;-MLw%bmh2L~ljaVN(`p@%owaM@bl8Cj2w96EEjPRukpQ{*L8 z=JOcaET{9B;u;?fgSRoO`A&?Wno#0$_Rk)vZj=??3a^Td9GjlEj-v7}fS(6L zU})Dm{OB-KC+1COgSk)B%+cf+w_X^wCm{&)S}n$4Y4oOVO6@8!U%zQ_UW<>TNTV!p zZ26_9_>(CR@15iua~n%KUX-R<%o>}xL0uo!@9~)ljyKH zi)RxkSArwLd!!-iRjSvHjgn&t8;RG001Hr0ZLntG_P>EJMUbPq{dNo()ozBr|?)#1cO)a+y{ z@%#RypjakKDv8%zECsu;-etQq(6?{?dl(jG+Scsua^}`v7pcbH8TmN4}#` zm*G`vME_{+Bi2D{y2H*<<&5smAORD|s{-IiOfFf-1J@3GoozL3N=vQ7ng$b$*eYXZ z8FHvQ-D*mO?UIJXu~juk&dQI@D757~koPFpV!EY9b`8d8+YF!&p|U}ny!h@L+!afe zdso~I8A&rePd&HMSuCth__?vGUy5Nu?brbkk|SL^HGyN{)@efe1ECs$S7k#5_OofT zNj;^`64R`x`o|x5(aU)QYi8Wvpf%iXV3Q)d3_Q7be9FH^Soor7bPevmF9ayR{kqz- z0<%+dr6da$nEY0qd9TO7c|D??KSh+SczpwfL=K4#qS0$9^+OZ7W?S_Z32?17*h9S~ zv9ITM&+*hH=yosuq@F`kE1ZB&w=7uEaQay3I{WRKTbNUrE5I15tZs8Ei)9~KULUU@ z!%&3AHhZ>}*V|)Xof0;%DrSa*yDgB=k4Qk$9Wn$XJJ*l#32URkk!&DGYa>jFGjla! zKPUy@#EL_*$Z-r@Zs0l32N(C+2XF3Wv5zT(6q6B~L4`}#Sz(WBM* zp24QRaoI|(?*g2HN;sDl<5y&Qj_oqF2A#Q@ZtZt#K>kE%I}~@F_IX$X9~JY(h0!g3#8Ol6P@7f!u36(z{VURx>h<};)F1MB=$&elv;&W&b`$3X z^WVXS_OS{>IuK-Z0>A%_h|zzj-2b1U3(p_BC4LA92xAC2R|r>E2m~>RuHAW*qsZ}0 zv?*S~iqtqknSKhII$A{m3MK~KEdeSTw$=e|DVE4Me;Us221>34Iu?qgIKlZM$~xZ0 z5sEtI?uG%DWRxLdprAo98pP%Jn}%tpASID@z($bFoLi!jPE;SQXzGUrIX_Md9~V?YP!GH z^`eD86QiQYF@{zNOfz}nfT~iqu7_)Bc4pn9zTRD3Uzdjg!L-y|m0A*c>B4j&Y<=VY zx2nz(5=*#zLWd*n@d}QK4D#ZEU2qJd`c5V@Ox4qT$mLJ`2xj@(Y@`i%J6?H;*?f55 z{?H_^U8Vw*S7KtWAvQ-ZT5U6N*kHttk&D!ff*CpNAI}n2eLpr@Q6Kvj`?~9~&1>Vl{+@{T3(!;IM?gRbgHPf=j9aUir>90`4@f6G~Gb?9y+l?9A-$>{V{zb@PluQbcOw^s+B!eh42w>>y0=^iQ;5 zvmA47OK%$?ko2&;n7}x)*@fFn$X!AgCM<3?);XXe>A#l7VC<{w?ayXU^1)le%4-R4 zrDr}jD6w7@q$@LNJh*Zyxvdc#4KUig0`AmgFT2cn8JRR~(F2(QTx) zWUM`fCb8=1ObRSB7EWx2a-~Ej>n&2*HQD;vc^xOc?aSS@wLSr!*wXmz`pID45m2m5lnJ$eAn$e8T3g(Ri-SldIOb8=@L{n=yHxxuZmAH0jDC%8euq zUMSBkq|iDiz?0Nfy>`kE`bgnf3+k>)_+Ut0L&yntCV|Xqski?EmzhwbU{P_a%;iqe zx)Tbul7Es5n#YG27^SziTy1z&W3rrf6B6hM(~gmrU5BD!8};q)`3nX*r$Jio-$*y!B}D)m-X0omVS`55&c+ za$H;j4r1jypK!hUGAhpkvYyZ)pMBL{SX_G|XlIrmLX<~yH}uFlUlU6)Z8B1@`f0}2 zQ{XKj#n_sX4EoQXsT8GFU2%szdmbi-nW!r$A0>t`R^IA1o6ORER3-l~CrD{V$A?zB zt#WXYiZR-1qSaY>-UsRnKZDWpU8$e z%q|?qf_Q3>Sy67`Bw<@QohCe_)?!Rx)X-#QRjo1_qRaCSrZKLsN^7ewVG=PZ63ijx z?)MK?=vIzY`)J|Vm*ghHvorqy8ROB=kh*{Hcn;-Pyhj#T^@xV)PPd<MKh`L2Htyu+m?)I{P|`Sxt0m1pKs77TKSo)ZTCuy!z;@V z3c_~@McSHFmwWC7C(|731O?S2;&7_<%nQ&G&&y_iHW!|*clmNNvoY(|6iK5}nP<&q z@udRe5Q|*#1$)+~!f}-^pZ)LRVZPwnuA0c@4lC93_{azjA$#D5pkbK3HfhzIU zdS)5@lBAleUSzJ*3Jq~z8Pm<|1L?*U{pnI?P9sXzK24SqSl90K;ym2re>HVYEJ+;i zQ^amAw{prgIkZhS_pwezp)*@se2$^b^qEZ8Jqd}-Q?zku?MVKWCKxs`d*K=(=_^%n zHrO}rpph2dy*eIE?GtRFzedeOT9h33xnh=S>ba($BG?c%pF>Hfm3LDOQm zr1*F0=t36;JfIf4*|&HqggH0&n;^j?lp!15+$iicl_8e%4YXarD2#v8 zFwVrneLo;x_EFOwXz&&}0?J&drp{!&`e>sX;vw0Q=PCKt!0T?OLa%7X=^)7=Ah#3h@)aqtTNN%4a zFSYeB)+v2^=aNTa(J5hD;D9iiIHl<1v8cRcSR`6~U?-6;S;3p7I9k#6Z6u{Wj1+3a zhDh;A@~oGeUKLFtaz1BbUc__~Id^t6*+-k^@rQMQ<0tnpo=SX8vBEnMyOAs1N}G#=?JTg|8kTw)YFW&RfFWCn<$3%ooH9+>8#?YV+#RjAJjYdu`) zJGx2qacF#l$#nMC(6^oLAwDZV0Ew;F?P|_HJK-JieJs7+KdX_wzm!)}Kl-67MkXdn zoP9s-MiTnUo|k}HkSL6lkZ`hm1LHk@jVHSOQeizn24iVyJA0TrXu^M&equYiaA4yz z0S}CxoTtkXvpxoHK!zSOPNEw(O{0dRU&YnOdK4u}7M6oB!rjfe9L#sCb1P{VQ+f_?(9=W?VpEMu)h-f!gK9304|^_=uSn z7?05RR-k#`2luA#QBU+M6%UM|^Rp5^Axr-B{zPKSzU4GOzj0Wp6?Eh91pX|<(LDT7Vl8AL`{uUc#XAjJl`p6D6-Y~L2Ej%P+1I%s}JQp4F5U$@6pH^lCjcE4Mk0;p` z$H;JTfbdhRtj1-mYVxJ7DJ%!ueONS)FQi0hp(z(#-}bk&aIWlnWX4cr4U*$xjj-~R zs&sL5OBC`F`HCl-8>JOzmSU?+-66Q<$A`;Kwy<+TD&AClK#+uhyINr>f(kH9-c;2apNdh$|R3^g*z z?Gb!l#mO-B*XTQ^^I_r#o-#+zuBj8j`VCfTx>0n>eeqM<=7_=_0##jodLVgPP@o~2 z=W1^WDu43sPXFP|P~$F3fBoiXG{<>eK4U^Os#?R*;%ttRh*E;pVKoiW*K}MlqOi68 z5gsD*B6asK>7+|>P4jSUjE=+LN14dY2l~IHj>sdM)y+1wN=0a^z_^>IJxbdu$Z6Ai zw*h%DCdzq7uU7LT5!h=0{AnJkn~$zV1i;cz6`cfp_;c%Rf8i8xA^UqdXB| zk03}ol#d+P9_~?MX(4_4p-)pc&C-1USD%RSlQXDnI`0nV)yWTe>aopur`Inn6Ph!> zIw4fJ3`^>4KCiq>lZWgc=0)9rv=U&lgEv-|L)g5en^=1}*wN36v+e}xOPj!;p5*bW zWQ;EUqzFmq7tVm~8T)Gtn`DGB-hpYSp&K3v!o2Mt$rD;;n7H^8<=y3oxp0jD#@>byNC(a$hllMCf?LONlSu5J+L7yI2=n(gzfb|&6P zyWtM&2jggv`;K`*Z$^Mi$Zue;llbnH!1T2Bz^|f2pYy1eLzxC#-;O=$R!J4>LGYcv zoSr2Y$;2Rtq?tp!tOQ>)e2E(Uj_mz0$2PbF_+6q|UvfStwFIFlm0Yo?r22uo6& zb+c4&O_psh^9W6A48J!Hnlh`Gf?vMU9$q(r67huiEdiq7l%_;@_099I?|#`2`|4ml>f=%PC_hS7Mh#~;kZX}o7RT2%YNw-4O_EJ^pjJ8UOP?o}nt&=5 zfbcvCUM?!;o!b?Ws`AunTY|GhDq1Cs`Er3Sj{-JL+%pXJ6{vWQyAcNnKUb2ehX zJDv2gfnA4#G{P0Io^nI;PW&+BEofix1Zmcg;-5_9Z|VeMG9qV5iS(}2nkl;5qyigU zw90ETeWr%U&rT_w#7i?m+7U)%JMqQngeF%79wju-O!Yl|EUm=vM5mv!#5K?MK`+_9 zOBKUDEn}=o-$|<_@y%V#=-x&tiLzUhRrAh{2a|}=Y0=^SSFaG@qo<5yJ?=D=t*Ohk zEuyy66xV9Q&R|~1=cWnHjqEFvbF?6=h%RxVvhDR7P8@UUs%#-ilb>M?#F%G0FI=}#k-SqKgd^g3qH0LNSc0W( zBJzo21C>>t?rF7*ZqV(*%4n~>m>GUz!?0qp)?v~+;_tN z^{Rf}W^RmMd4If@Z^qz#o1tXt1@|*sO_&zdyZTuYRYn6(5Gg=(a6ZlvGUM#96${6UFM_&hSLp`v$I zN>&^hj|wu$@q??@_YkD@-_D5!S<5SveX@fg-uTjDCRbv&BF3uoHhdIF&92_2bl=$C z#^!o0_PpACKVsbR_M&RzuHoAf%Q?derCu%#X8~ExWt)U0K{n-QYlA_sSG8#=ycn66 z)OuH@=1=(fc)!X|D`8P=iwoV5mw28%L)b@G-Ru+x3jK)ANGJI`SBs^ZK9+CJOX+r` zCI3^6mTT%-?`f;w?7!5c(zGj=1@=|43NjAQSl8MMIx_dJiu-$fx~J;w6uXG6-$+@tW%ZMZhj#}SVFw)ovd z6e}0fc#e=$-cs{NN4}1AZU_2ZI6ppE0S+Q^H}AJ@%4vM14`#Z$4r7yV#_!;q9F!&Z zOCFL6@XviLSI*E@>XT^jmhUP!J}}Yu749lHIpAOtVED-D>)q4cFMVjF>v)K;N}wNc z*1V0?A#xauk;%&uqhm8Cmx>ly%RvxICYu=+EN5^gs9|yA7Nvf_&9k)frinBJ+7UG` zK0juBxU~VUDF6Ls{3zl5humL!;Mtb+)}vTMzMv+jX;^q7ulMdwIudcNEllrO=eyan zB8Q9R`PL&yc$PQm=czCEF+nEgUw0TVD;f9P)S`pPGJ~@# zyO~pcl-3Il>1($s1>Dg`JB%@2ZHET)8IiWG8PQIEVKU8CLwkZSWnOaV5n_c}_dYt) zvXWUoq6C9G0dy?^mKZfdA$!U0Ws~n9lX163;1m~3g z2Uk+?OM%>L$CL)Hz4j8dcc4_Jk(nVJirGpQh~8N33Zou{(d99=99r7yzSsy}13yO+ z#gjy4Bf1xcw)PH#FBV_=>)6oovX%wAnhnp)vquay{Z`ov7U#Wl8lXkAz%uZsB>q=-EsH>boC zH~)N1YBFXu7$vcG8GcJC^br3D@OBk+J33$&n9*+QR#Lt-f zh~rE%45aspgh7*miDpk_Ix{Q{&`0mkCg419G-%hzzUfdfHE;#ZMXY#mGsJAvTLWQM zMIz9*IB}Em0rlYX?fj1Ss&8)UC(l|J-x_#GEYa%Xg8D>K;?`QSq%&5;Z6)!Qw@%kS zFXjx@ALk|qccYkBtg>J~&W$%O9VQ=6;(2+nxv2OE=^^fl!FA)lgTn$@{oyo>G#S1y z_?-x8cqM}u18s3Bh&XWdv{|#pD4V(o+wWg<@T1nB5fT`Yf8K>byWU`b5=V$D4006b zPs=C5&$RBJEf0-3PA-qvoOJ?9R&gWgB&h4f;9 z(04t3xoZKFC@g2g(d+73<<>;Btk@iiFVJ>)L*+gxwU3sQaB3cYrjYbu?CiIk|WW*;(2ex!6ND9q}LJ z0k^chNoBIMm@Yw}N=!)SaoIYY*JBMq+55Q~QI9OF=B6vAn`77e%y>%tUhZ5)XY#&u zv&+Mb)KFJCDs_#M)x2>xmG#!{?ilb=mVVC1Ikb#A&5qy*91qJpvpl9M3UFu2ip%HyZE45E?aM*1kEHM_PH~cNE+OKjXaSP{|0>ke` z)PdGs4$ZASDs$@Re|#HlAjLYH!)R7vZh#w}>A)YyZP8cNWctRt!OkrZwULw_R4U~l`l!E>^VqEF z#=Xo|pTd4Nw5pT+re?)}wwNwoxYL?jB)p4S&EyjnpAh$=ZgG1Qzh*nYRM2d&zJgtr zlAl!O^1%Dm24RfdC-j z#=|Z;QxEq2G4rh=70=hm_7Nvh@Sd+u%DDS|e>)t)a4Edp7tjnDKr;;fpczIcCg6&^ z#+EjgE}l$6zy6=Ry(tJ>)$bP5J0@vS9$p!+L=>!dYB zFKSelGLSd<6pvvtG3W zQ(kB3mRHSEV~g}58D*t&W|IEPhwGuTU5*^mkT-1gy*Nu2r@MN#c#6eGEcIp+|KP}eZ%r1Y>sc-7E@jV zfwUSapsz;<=IU)x4mond4Xot5uM6iejNL;&ct82@>8XMdYz;L(e!2-RMJl;M8{5a+ z&Qy{+Cy0o1yT$49I7mz+IHXZccSN7-Bq^#k}6JMxo>#*uo^3+a_T4rVEy1wy~6) zyTk0w6nsRSF%|shaw97q< z9GDd0c8N6AhE5?RobKH{UdD;GlPm1g;UIfWVx0b2v({&h#=L9{1*3%|5_m*94H~?{?hKS+m+wzX;zk3>S6bw$pcPrxOpw- zj>+CGDKor+rVUXr=?iJMmy?t1X|zXbe$w5_(@xr> zZ9WVhi28->W@iN^7EQv* zCZw>JQK|~u(WWc9di1gUC%CSJglK0hpx{&JR0btoD`65B7TU0(7J1Ln?^+n_<2ALL zn$*yWkF}`E(7}04V5aQEgmZE0t(H_ zUllA_;AlY9`IyZhd;CEx6AjyC&qT1L*QAD9r0p#;jEqqKc6X8CM_Zx!=M(D@6C=#Q zFT%sG2bped281`Ba>G-k0qlhWOhJkW90hZf-x`cEONzUuI6W|z2Qf)~W`udRgR zSr4v_iOiWGQj@M-M~Tt(MED$)NrPvo9#3@z?{1fr@*_8=9Upv^YSQ&wW;w5P(M%!! z+_BJj^dz_jfq_i#Fqh%h$ci%D&RH`Hp^lQb{?S_6aB)3SNXD+I#Z(KcCfby5R*9xi zO8SDaWZ~gDMH%{tXbgFJug~J-1mt;GQ=J8y{W^Otn$F;E~l6uUB-9eu4@X%8^wyHMxh)0%EzSBNpRo2F^?1o45-dswj}sR8bx&_VT12huSQ%5mVz>h= zh9ZAj48OTX;HMuHja)3G?0^8v!5)aS&Yh)S(jJ6cWVx~o~Zl-bX_B>== z8r$3yxLhw<0*m9tn2{7K@;b%G3pu?~NS#Kmw+kwh>&%w~68I3+18o0u2?D1~V(Lie8i^M!dob=HHjZ@J;18?F8(lO!o(_5wR znqT;$Tu8biFW1HxXcb&wwU}+Z~h4xNHy5@PXptf`p^F8f9Mw;=Yjw8&zj$4&% zh`U;;{;8@gQLtV6&2qe7rCpL<@RDc^U&m25cg?^ww~r^!(k;EO^c}Rj8+QcuOWOol*XU2gfm(Ejmmf`6 z1QV$$=2Ee@Js!>NxG!y3m%J+P{FoW;NSrL>j;Y)kOdV!@2uq<8eqdd-M0to>aqlJ1 zx}_7rQkmG}F}z$0&i$oJ#2Y4jS8Po0=l6z|vfU48qe&AJ9ziG0RT_Vl<_av3Lc4bD z5*GfpxG{YNej%VdI)L)9LRTIWppY1_6~_d=3I+1`^;GzIK=lp?s8IPZgr{IdlsA%- zWWK(IE!4yEfChBiBC&hp?N;7dll4mH;$fC_ zW1{)vqw&P0nCSq;h4dNOv<^;t!FwJLM8Zr+X&8w+TeDSaN(4U$<$FmW!Epl-g;b6-KY6NeYOTlK%+2oX z_~bJ|GjWD#T)Q|nrHy!ZQjjguoxq2j%2K;YvGf}9*9vPl5n6?1XdT&>Uk35JP4ZPO z=rn%XlP6@z*+eqwasf$tOgkkM_^eFKl3eq)T`Sk9`b0;b@VxTcdJNef{3Mpq^d@|? zCr)QFlT~0%^zEa`L-ZE5`Dk6$NE}w`iy}})M;|0teDIW`t;pS84UW`nYXr+Lt`U)emyF(kJ2?2+ zmmtKvL-FP5@qN{!s`~C%6AF&rBI2x!kp%B|RhD|*7+p;Ya=O0zWT7ma)@W1U6t>kk zu-q$pb$s((K`VRUx{ix@*)1=k!~EGB37k9*t%JDlzKS9~R?Aowx|vkK&DGdL(>OWs zU?EuoMmU%~mGZf)#yx-EviN43HB>x}*pKX-F3p72Gt-{*&owZ;o6D}5t3@dE#~t`G z)Z>OE2Qzlg+ogSI<)iO7!rqv_EKyvRVJzckqQPx-?X0;9vs<>ZdVh`Z&8IzO*=N^% z#P@naGg|Pi!!#pyD4s1y8S>@xy&~)8wRTT=*&KzX$=|*v0BKrIKlJ0(0}6H2WzGg|m_K2kA)yq7hl}AAM?jF9A?)~=F@GE$M```e+HX&D;qBfO}?~ZfdrtYH6 zz0`;uQXvdSpURHgs%UE6+-2T64IvTC zS}(5zM_uQ$xSO#B0uI9qWpP%_J2sIKcI*dAgi$Hj;yNY2NgyS1I27(|#)t<3!DO|a zHF7@CU3uKAeaIOR`8qjlQODQyYNbP($OWp}P!n6p{q1j#SW-Gt=VGGF-E=%Gq9L-n z()Nmb9f{L*2-(q!d-PpwifWbf3xg#TyBP$y5!!&7`P!)uPQ@fM31+AC+G^GDbuX58 zv#L9;4D3DS?X=`1N>S=6Pkv$6smkD9{@@Z;22(v~Ifv(^twBZ_hb5cAL3uEreqz!6 zu}4}}BSIiA!lgN{Pvb0(aQR^<$OIl%bF|{Jk;Xl*DDq_&VmY7RzeBZQ9A|WLFF(ZS zv%!?RZH=|mm3~|^#sPs~Rg*f_oS}O+mGB=SJs%Fm^yyt?S$=Hi7adxGBD#DPeHZCN zb7L&F5KxRopGbmGYwv+uiEYqjWGiVH5nQUPD!tdWutZm=J{&#^?OD(cb=y_NR(ZrZ zQMI-;h&jvSm;k)@du_$VpR`8W^VJGxPUrkYSez!(bGrk2ELWX28Qb~mydpuS?7RU8 zgJjxuQrfE4FC`-e_~m499awE-q~UZeNZ%9L5KG|~miOiIv?!;vM{XeZ>fO-ICeWam zQof<<=TAc@=Mbthx+$nUt9_f_)dHbGe~_Zw79&0dAK0vXkXd)E(YGC=7%k0cByTQv z2CG@%d*k;#)+fTd?asiGn*vl^5&Exl16o!yFKNK(ohWATnQI!g+AsyKGPPZE#dSAR zZ6PVk!UyPc!%+lv3`6pGrGn2BMPCnCQPc!UtYA3k*a^d%rOYOU7$3Noa7EstvZIS} zi*fHiY|5D~e{puY^hMyZceK51&m0oQn4<{00e-b*cDUMSket@bgwKgDMiW0*`k`QG z4I|s&S2F-<98`c}J%r{&_1A_?HN-I$cq!G?I_J2eJ7_92DXwnZ9!X-vd5Q+BullLham^8n z;91hU=B3Lr9T6N}b#-ESPgzstl{1=O50S%~WjdBJk|*ZhTtk>5qI>vpyQ{;S+Dqx( z(<3X9om)72iMKXX###v%QEarhLV|F&h%!QeFl-bK{ABR$ourVY&FMgC1 zgh)6tg;6FU(3;-4elRsk?wI)A$8VJ&O()~#=Pda#l>M}$I|}t~#xs6`vlVi4;u(#j zyfPn8@i@CAI@Z5ho7T;?ukxvl!sP8#FBz=_W^)(v$joEm@a>-EguwLeu$M9haMHoU z*bg|3Bke!fw?}On>?G-MlfJRI&7t#j^S*mo^C@!o`ZQa$AA&Enr2#E5nKPl?3wZvl zo)GQqx9)9TT$zho{I7$m7~gg&y2YLPOx(Pds!Hp2+Bzq^kBZUOmAq^)+(qZp@ApE= zScoq5AXHsk2WJRbRvdT+B{w@+dGgDil0MSDK_`DlWct0ZHwL$Dmu@iUHDlnXncylK zw{j$JvS_poAf$bQzk#mV+`;3AzE02LjM$#ThN$j|Aksn`l0qD;N4bV?#Cffb8{O!yDu>NS$Wmu%d)G16EC3QP}>TDR8k!&gE zl$3)q7Y&|CH_p0u=)U1i%{T_Kvh4f^%K^Ya3NtqvaofmAsypUpO}joz-p+%*C;Ka` z_r);dXHsOcG4A>kkD0P%zJkI2=tdI;(oHt!G<9JO)6pD=9V+WWHNaw@ea5`N0+(Y} zTXp+$yxwlfqi1zV1vm`-rFE3u-Ie+`YlBs=DHRNGd`BFdSdWkgP~KJG-bKtd<15m* zLH{MpQStGEMt?mP+M{5)1%BShub^4x;!5g<+WGO)XPim$6LMo*@eU0ZW7hegt5&JA zE3Mwqq{3J%ktdHGtab!HvLIL4`jdKE9L9X|4dOAx?L7`hHYoCCKZ4~RChm;gqQ9rX zRi}LCktY@F{D?z;2D8ihq4)}xt%wOasO?UR1Pg26GWlrmRt)D_k;%aq!UCtBvV=EF zyG^UuCGlse*2*14%drs^yU6K&Gzvj0$#@=R@1DOhnn;e88hSJ3LeSU@gBS1Y)wRMv zs)K9ktmn)W72GlqWEcHTbG|x&zC&+nsI2d~pIr2jr1kjRcsq4R^eb6IWs!{e%fu|j zr~MVTi;ul{^i-#rQ9exWwI;1Nb-raC85O!A*B`t-O8?R#C3}OW6T9uuZAd6-$bq`% zzR>|Ec|q}eWp@p|8|GZoW`S2)(}ymvVpj$$p}pAPd%ELK>KZ?RIvUsL!-C9Ao>KI& zDYNLfhrfL#BuK>9IJD~su12VimuwHNVr?z)hT$glTC>(&DoZ$V)DG&qH260Rm)p3- za!7-G$>Ihs4F*yXFyjtxpldIFF-rxW4QiAK^tZrxuHt7W#w9c&JLoGNh72pk5T}o9 zltOK3SQvT#E4L8^tyu9k({fz5!vlPaxk%F(>V5t>I7Ssmysk&2jT1L9l^KFC84V*- zI@GA{NRUQ3Q>!Ts3i8CRCdy!hjlIpbM~$ktEqsk7tAWTXiGbCfI8Suq{&5zQF}Mdy zI!tD3t8>}V+V=tBQ{rtUKsu^Mbc9WiI!){6Mq;kgQ7XDb(Nt**l9VyjG*A+31x z>g?)gzVLs|L~J0WmHbc!%K&7EtAJ|S|Cf@GEZ@VMbah)5;K3hXj#zprY;?ptl*-X6 zRdfr4L3IWpdeJ9ge#!xc)+KiFNc1*t^Nm?28<|@;2Wm~ZD(Q}2Co*UI9omzZ7@31P zLsA~5dF@XfrFr!^_t(@logNM`U9xM7G`jXhjUISUEj!#BxuKfrHM}yt)N}gR0bKl| zq|s#j@!7e?VvlZ0Kc{+4PQ$ORr2f2=gvDGlnDbfIt@H`Sx2Z3!g@v+BHH}A}X#|#+ zYRGE$C9W0O*)2ztX6m%-+jlr%ElAg@IosqUxt@r(xz(?orrYQZBlCSi9Amekt20~e zr@QO=Y;7f@^WEp*4FIm#emt8n+44Gunsp^n48ZAJtZ-`)E#JsWBy6dcB-p z%eEhp6;jQl<8ur{UdI&|nnE|gp=W$igzQ4DM~$5vL4>q>r&a~q;Lv}!W;AdU&#cO{ zni!?yOHW!wl=^4A{!%7YKC?m+(?}ZJjV<-9z#Oltw!B#qV>oHQ0ZyASuKUztaBsq! zDk%w-bgWa`>_kHG3zD&(+?~ldwrF*R3GLT?v6}>8H)ec)NIs z-8}P}jz+`!&oD2eLPx`p(>oh$mkG?|xL#q=%W!$BxO)yG+Epf>bsWZ2zI9=9sxHpp zJf$glPEOiKWPUH>z?Y|ZqhvYiQhSx+HiAr%jg3z2(jAwIchduz)%_2ftI zTF|81s}augI!Sz#0%R98Wf4MLGBp(^?&qGM7WxaGA%hg5dPX&)#o6XIH~Qf(nW7|&K) z++q02jnkpF$P!vd_zW#Qfg9C}oMb#VW}C$PfqwgyR=KZ_NJ5lTa~aSrI2|MEC2hm2 zEV4k%L_p~WhRF)S)LER7OG{4|wrnZ3@*) z+IfhLwQ>zLy+`EAC@BjJ8jKO0xZw(AFAmC zuGsL7TF!uJXD~}GPMlLmgG45;47)iQw!cEqokjP6FhcR(Xz_9l{e&T|d!qOqTxHk8 z>fZC>c3I~!-x#g;&E{i~Y%%8gYG=gfepPFYHSO%BR`9XCg>k8pGnc48zeEhK91Kj` z)pV})$bHijxV1M4L*iKv#KtSkl3;8G5mbAPtzB!xtZb7@o5fN?Sh(qR|8!};I$6@QFeiHe>*I&dELN&=I{ zJLNpzEJX&_#|%2p?Qt=REh#1{ZgAA!=vFLvTxaUua`0>q4&!DdP$OeL9i-w|mzz>; z+TYXQ-2d?C`SJ1R^*i^IZ|`)@TVCzoku=m1QfrC!UqW<&t3+PKTVu&Jkka}33@`p} zNp~!d(^r3^k%THPJcIo}uXZny9ZcDdxi&`kycZ(oU1ubG!R}Nqk4njpf;ow!2*)_7 zLfS>cW3IlWpY?okD`fD@v|_0QqxOl(jWqHlr&lNYk{qcFwTrq3=8T!w#^&2Njc2M| zdZX?1w-7jkr}#PLy>ME0bSghFc1gr%b+IoN&%CDo#N4Iv8Wzh1L0iZ?;Y}HubC05q zd;!L_n&|llZ$)cQQ*L}f>_>OYeCJ!x-^_#jX_wF9hWBiIf89oRlY4t9Wpp0C*ysF3 z4po1AJ0TOU1G-Ae;M~Xe`B22x=qxNevU^^zbX(jeJj=Xs--uySJXoZ`?9{rEEk3)^ zFgDc@o()emHm522F3X;BPP0R*+trfD(ZsJBWlR-PGuMrH9Qd1QA9Elz|Mip0CrSe^ z-(jP{4K{mkrgCobnH}hV$S}C%Qf450wa#;RXPkV2Q8g=DMmT?f5PdCU-@7$B2jeRkYhj8|tX=ei};&j7pNUJ1kp~>KU5!ZHlRq#qzJmI~}s8g0>=cJDeQT zIr#W)fLySO-0EbW6l;x-nPvG)!kM(n&C_jcM%p=JpgrD_LXYNfPT2T7SnX@@I^!#o zJ^#b5MEW-IB!~SEF&IUTktuHs={LkxK14ZOi@1fs6v4R^(9xD|NYQTd31Kfqcd2F( z{Y5m)*iOhLu>{DFp8C-$FhA(k1Y>GJg4%v#SI6 zg_u8n8w29oeNevb#`Z@7Kpo*94Ge!7(Btz?pTh$LfF$qVh`hwhRh|R>IsT7gMdxI4 z{kSeBB5&_vX=Z6+1b+PZyXp%YIfE$F?3_SGrohgX8_3D|+h2Zn>VKEEP>EDIcpD5B zh~U91^?A$n^9BCof{rVLZ0%iuLqrberINlu{_>~Lu|h^@v1!2k0eS`AayxIiegVMR za{(4~{-6Wg`5^lhEUI+?K=_g`~b@J%LOh=UVsCY z4~k#@^v|*bLjZ8!8qtr>OW%wIHc%oH8s=Xo2!645V7sG|*amY5&~gO|Dro*B1y(tg zewXc43ZmIQS3j|~5z574nVYU=|s0siKIA4m%fbMqgt&V_rfqR;`r zGCZGIxZwZ!*1+rF4s?KTp~dg&9D`OxrVWs3045c;FEQ4ehQ&+GPa&WNm{1xO}!=S@>SH6)T0M@lY;JhaNIS0UJuHPW}#W3nW zi>`pzROpnYo9b%c0iCA<`~=8p$O$-EP&DS7c%a=dxfr81iI?gX&|3lk2U1ha=>9Fn zAJ@)asSOn^fcOVsoY^46;{{0TA|_NV?OecH1;R!q*5<%P2Rl=+z@Upsa1+@PybW&* zOmHy>1S-ah5iSzb`HJ#me;zs{7zIH5)&O1l(ZK32#(XiP(S@sv{%#4rCZI#N0S}z- zoE-3(>lgCx=>2xy?VG`X-t|}YK20Bh@i+t4dtnGD9?pN&_1~cW$-e+YoQsO^CmvW- zp1=wQW&D3ee7{?ZUnx;SP^$a~NM;I%t{7os~8|DLsS@5Tvz{-RMOpiZF{3j07 zZ9C{_)1!CN-2jd(f!PPCxheM`wxJ9a3LJd>>Z-~a z*%_Ii-?|esu`#lAg3gpA7K@!Mz|=ipPC`1}2x?G~zFA%wkf)g2&zc^8qEF?of%*E! zBn02{I&Zmtc^VhfmjgmTd%J%DfeARQ%J0?gjsk)@2G&-lve<1Vot+QXebTB#I zLoDu3fLzxA#vzqa_ur6%`sgIr-&F(*MWGt2&j&z>08)UQ&Dn-fd;U#DzRTCn%+lP| z>6cJm+{xb49y$(KDCdF(CtSWau>IsThKh53n)UY)dzlhO;|X*a1CR~mO7HY%LJ-Gd z|Ikwb>95<90024Qu0TqJ*A!|`f1jS;=f8xd9WY0Y!D)?umI!1H#?A~XjvY|;`FCk> zcoa=91DyqDKZMSu3VZ@H@M0o>B<#S~r`&+cRuH!2KlK^^CmeVb*DRp+`A1>J-@83a zeYfBqAYV-&HV0OypDovq%JNTiR7{+}SHFJT8vMuZzT}VT2LaNF12hj(SB`C=!u+D# z;F8+^2tcZUqs9mTkOBaZy7I#QVt~IHz<OCBVOL8VAQ7Qq z>09UKyY^; zSbFLEM+AGji-ruk?5x#qJq!j|=>&8TGQ8pqx)4a!$k`frS<}S&hqV4-e8Bw&$bV~q zkNKm4=RI}^R1ACPznLmUS=7L*>e5rBmOra1v@2I zkdvn>aBt&0O#jZZuoJl6KlG7rGtl(u0>CAJZUFw=&z9>)5cR)yL&V13`L|rupSn=# ze=30oXoe(UA|OSs8U4R?;Q~huV`Fg49{`C)`+ddeiv6E}po{cAg_u2f#i#;!g7nRE zlAywTUok*7_9lQ~P;~-k)cNZ4Q;VP@2~3@=fzKS=29TJ34jtI!8K?XK39@2>qzH84 zy{9`C)4(iP0^%pgxpwyOV&c$p2^S$G&R-rT2D%Qul_CbweMTT-2PHcDCMf6#zvU_} zg3O$$>skOL<_a(d8Erg!d@-aP$j%jfOZ;D15QY}wr-A@Sh`^p4Ed*DKITu6xVmU4t z50qDLyYB&A?*b$S*>$_Zi|IlJV*j8|ttY&-VE_c!F9tFoKU=P!;8Um&=MFZ6U;Gas zwiGI|ngGZnfDg#EhPdR9AmCe!=T##A3F8&JCU{~z0Wctafv!@h7(X*DKjZ%MNc=}K z;~xN9*Pk-w0)XJ6Rl+~z2A+vtW&aWIUn$vc+JG@QYDNb17;@^|djS>iuY}uAPJn>+ zJj4AR^Wr==*xm6pmbs?}$TJmiNK7DDEU5TDLsFdg1ajrNF!KoR&^#x`V``v7UO13 zS5}=ysQvk7VE-0c{1nB7DG+C4GZJ9{h!X&UjA3}1p#uG!Z{Lr@|EBX7;tb}LZ?6M5 zQGc6e!!1y8!13;dq3U0JDPQ2E!DxD3jsbZ345%1nQk3)+R6MZH3OIrw8*s+{J69q= z_zr|e{|x4kC};}a+ExbUvK!1RKYPu=Pt$^RCaUkD(%7owa30Q3R<7yLPxU>|OO?LPug z$a(&i5P+_Nk4mx;z5qy0fTe(BXKmw8XurQNb0Is;!wp(406WNlydZ~zZ3imA_b?P( zU+tH2>=$a&_Wblt5I~v|`S-=+%kIVGA>q=8?7@zR6;-LhvNUfhen!nz9uDOH@UR0psp z4zPC{g1wK&7ZX+l@7}3ey4ZkJT%0T&F4O{&*<7t^fGQ$@0O^@+oL)>F+-)ElfT*%} z?wLbZXOPVxQ7(Wp2mA;EKj$si@AT}y;Xoc`x&SS*%LjbUp%k!{GUrghXRhBR;PB~1 zD)D!jQK$erEH*%{$_^xCVw8*jj|Um^x&30w7KS9NhU<*-p&E0g%P{!2L0pdXi_W&J0L& z1&}CYDoOI%e}INAwel}>JQRS$FoC#;AA+xTKrb&EE~r!Vr>(ds217+y*uM!d!UZq_ zx$h|soVmOR3!1|J^vEuPGG?)s0xx)6U^oc2no<7@1%xEO?|WWoAJ{c`IVS;yCwtx^5>{O&@%2K?QN#>?ne^Zhrs%&xUTb{F8H@Neb;3Gd(1gwVdJ4SPabno+_mBe24n080&?C{FNo# zpWtAY?z2J#2iGh68w&&l{fk9__B{bqf%YdAFbmaxpduxr_74K@!pQst2S^;8=!Z1@ zMVwH3_alG=2Y$Z=YjVoW7nu{mUPB`+0EPT-C~!Rg1Ih(Su9^+++9g0X?!Zv1L&(U1 z_kTc9a{-DC8rhkE0HbdXR1^F8kT|fy0lY$=u4wQiwjBSjwJU*+`C8*+k6K!_#!jd5 z+C~vUd7jEbNF%Q1HWCp_Q$=g5P|{Sj4^5svT3gXlOKYiC7mj$^L=hEB zt>y7N-uvDE%zw-`^WFbUhB?P^j&t1K{g(UPZ@quWX^fX+QQ$B-i zZCV`7p(sW(B3E*PhRm4s#H6&)KwieFx9hxAgw5B&=4o+h<3Q=6j*SeV#-yaIIHSSD zVXAtfBV=TU%BoB13%b$aA>*#La54E}3QBokg9H@+d z!TLde7Q#M^#?{(`D9nv$>AH3UQWlf$Lb##chVF;qsx)c=<4!=%j*^1dbIAckJ4JnV z>FKuOUB)b&fsO>N?szGhDs?N88z*c)z8!@?`T^4_?LmfiGg09g)5>K-?q=P9I` zXV-|jyf5XA6Sa|82rrm0y4`|Dh2=Hn#A2`X!*GljH*z#T#S1+|)0jg{mYg#2C29E(AF|u=w4v;N~EqzJ#L-3p|BYAcGKYC9w0jB7ClK@>JD% zpf(csMht-+{=TQMA;$Dz<2XY`l6$8n@ASohNuYc#q)Wqup&xp>g`p`)8OcUB6geYu zqTv+ihD~#b^Ze0|;hD54IIm@14ZL<_f#`2wx%(5oPlfa38LlF3B~Qp>=9X++BgS2Y zA4zL~!{GBUn3~mZ#X<&3y`0^C_z@w@tn&Ay#b% zC--QZ;e}ApbG30I2(jzGl&`B1&e05y3#tz9X>Z;O(Qbj=r9tzy-5#Uk!d7PC-8?Wa zoZj37(Xnn0_CXVWSN1*jJrJ!_S*4ZutKRsOk4P!}jNL^L`E38jqutqu+b*PYEWiFd zcx<|S=-m*4U3kDFY(cO@I{Z;QdN_E5*-AYPR+b!Mcc8}nC=G~AmYoX7BT+#axU2kH zTK~3q-^0+u&Rhs+G>M;#>aLUHE&j8*lAf%T?_#C+#58k~A_uLRmixYE%C@wFo?H(Wb@z*1iF$TUWeHJDt5 z551bktq~4&5NraSut`4l2pe$23)&KQr|NZ(Tn3E`A*A;xc?`(Bicblsxi}sNtR@UF zbv@^wcm&Xa$YWS$%S;KbIKHlE)etccB0e-v3yblko^OFC_w}jXdvR;)(sAbuAUxiI zA#&!hxu;$(!ivSGns2-XoytHoLvx2mPqV1jrW%oCw!1^)q^IVmY0EFh0AL*?Nez|v zITk?i^FmS$52u)1AI<$yb#H73NC6xnn%&xWUJhcne>Ec~C8PSDZ-0mY!c$JJ){%>H zgm6P@LZZnsQGl)&L$aDfbBT8%H{og>uxu{mLNzZ34XTVbNT=x@a!=NUy0mR-?+eg4 zWT_pB4QH>jSQgDwS{cOKUrIeLZJw3AFC)58n zE$GDY@Ah{Uva)JM@B5 zwHG~co+0V|d8`Rt{o-+{=*~Gwt9DI~u_VQrZ2|9vBdAq6l<2+I(@O&@)tcl})K4*U z53_RNseXDlk}A11K8Zw3rW2n}pf+~^(xy_5xw0fMnG2{jt)yQ)!bQ?v&0TkWektu13 zq7vaw3)h99xL7fC=!*@})OQl%^j*e?T2Mgg0J+>{0wnFv$XWi zlvGS>d~n}q8uso5wG1YV^l!leD0il~&JaV8raA{VAIE}MlZf`Ocy!3;O18}MMC!IhyD1Pfg3LHVJX$@&w;{n>rkO_)>hVi<|X z#WJn?R6Hv}r8dw6S{eUJa~ZMN0AmuY78GEjAzkmMXZuwI#usa6?Ef2hbr54d)XlFK zptV&FgO^CA`9&Wc;>`!BY|qS)tljqpnT6L5)Gi4^GrkIj#?#-bc$5+g9CbT}dD_!NM2t zhc7_;MP_DefrzG1g%)>#GMYv|jiR%z9Mn*Yo|(e}DNY9xa!JMzV_JGfsxjD@juOB6 zZCfqQW&U=n$xL9C0E=3v*jzc5;={#f76_lh&Qjl@E3Oho{0cP+0_z)w zh4JS)cfvouMo~v+gm_*gR>L$)8^^Wi=ajvkDcYDXfd(2foAb2S=Kb5SFs?!-tVheb z&s59;6=9$vlu&VWwj7B#eY7Xfu3=hBN^j3^yQ?~=TY>6kIKkbCYKlxs8n|jj#5HrP z`R<)B0-q&da591Ro$oO;txZ@cr!_lugR+v1kVc5ncg%ilEeM>U{?3C19&hboudj7m zQKEI6?}!nbI8(>;-34soM{FXN@TVu<;Cf7=(Fch)z?2~w@vw({H!DMR=gvY>b&%(y z5z#6L(8JxZ(vo;2^;!i)2wR^6cF>lnKbJk$4e(~hFX{KZuK?c&@S}({&M)^E-bp3w zq+uGzy#6Qj!Y^oh=M~tA8K#1o?zZo-J7FtoT1MfQ8E-r*!m%b!mqVTRSp+uh(?S?_ z|C!thAanvZX$QyRmD&hS?~=GSG*>o`93hIJEy1w9M0CejRT9iPI;KV17q$B&)`oK> zK7&E;jO`z>X#9x3_%MBE?Fewu7ty1?#c}M`+))u)eo|Q^Ug?K+{c_0r5RpG_2kO(l zv_*>G)M=`a!UOEN;T`oCh$R7)LaNIhc1Qu#zE9HatA0SzzKdI&mQ~|Uth@IV+88@J zx`?ZGN)h^}8WMz?t5gn@OrAx{WFUtNmUrBSu>WT#NnFDaHDes*GM8At6H|ODe4xX^ z8|g=M_&@)WBFM?m_cLSA1qJq0;c)S?sq$g@&H^MD_CfeG)t2{*Cy-16UkE7w%N_eh z&!ow1#+uRHBR+Xg1;-F1&a?-qj`c0FKT9=p8L}M_iRcI#Q!o+RfwpB^i|% z)LIuCm~3Xc^M$Y;EC`+-3KmXu{p<#qpFcMz;x0{SRSi`$;S zNDkGShJo|yfSO-n;^?93bw>*09t&lb;@7*yeHtS()V01NWkndOuRH@K$%wP#sun`= zbOpE`=>6KLN;M9~k{nbjRBBGnqVaB=tMX%s33VW;(>PZ&(i?+M3o$g=fdw1W#$s&$ z;TMb)JYM5x1OoI~Aitjyd zcfq4EbZGlDm<1Uxqh=UIq{ovS8~(>nhk^PgNFGEu91rhVs+20pYW>qP8m)}#S_#fb zOHWA_rZa^ZCKL3IjlR|jZLo@yPOq~SZQMXs6qlh_8d%gIwC0xwW0U zpuB`tPRIl5UW3XSp2caYf($-Y=CznObjfOkU>2JvHFIINADV%`$MJ_Im@0f92~OiR zDCpOT1a_K)s3Z<}~qe>_Gxw`=k#nL#! zWwC9STn2InAk$0u?>*c=R#nLHF!A!8n)KVn*|_aPnmQ(rFZXr>OmcpBtetoKW~_ng zwnKS~UXVxnu~=rc62a9EWUkWEjOp{lm*MV$h5d;wIy8tyvDObPFSJ{F+3fB~{XkQi zG2J$V%~Q58A#1o1zHJE#Mf4iWdBp=v9%N+2kWK|4NYvNp&EGx7T@bm8Uc4&ud?F6{ zV6ct~r}a>+)g9l{8iZRb(VSxDX*gf<+UjRvHOo*!qBNAg%A(k^`xcRCul}{~?qt!* z`X){etg2I{O|f6!)C^`>QMq5}3j z$f2YxN|xh@7}l=oMjIj|-2W|1tLUx*qAxk@(^Iq|V1SuUB5kyex_O&&fhMXgV+h)c zNpdu|s~%RL$IZ<8r@NPB6+wb0k!PVkS3cVPWQtO`&G7m&ynf@Azk&@nz=j~ghP6{P zkRN`sjQ#GHLOl-{=exN%hURo)ES2UH&t|iLW5o~%nmEK2ZyN^D00_MGpg_!#ioz)65Q z(xZKOA&aHl0hK!15@g5EsH;k|dgkr(ST&#+$fH*At= 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. + * + *

+ * + *

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: + *

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.